프로그래밍/C++ 문법

Move Semantics in C++: A Comprehensive Guide

studylida 2023. 2. 1. 21:00

Move Semantics in C++: A Comprehensive Guide

Move semantics is a crucial concept in C++ programming, which allows for efficient object transfers. It is particularly important for large objects, as it reduces the cost of copying by transferring ownership of the resources instead of copying them. In this article, we will explore the concepts of L-values & R-values, R-value references, the Rule of 5 & 0, Copy & Move Semantics, copy & move elision, and std::move, and their role in move semantics.

L-value and R-value

In C++, an object is either an L-value or an R-value. An L-value (left-value) is an object that has a memory address and can be assigned a value, whereas an R-value (right-value) is an object that is temporary and cannot be assigned a value.

 

L-value 

  • Has a name
  • All variables are l-values
  • Can be assigned values
  • Some expressions return l-value
  • L-value persists beyond the expression
  • Functions that return by reference return l-value
  • Reference to l-value(called l-value reference)

R-value

  • Does not have a name
  • R-value is a temporary value
  • Cannot be assigned values
  • Some expressions return r-value
  • Does not persist beyond the expression
  • Functions that return by value return r-value
  • R-value reference to r-value(called r-value reference)

For example:

// x, y, z are l-values, and 5, 10, 8 are r-values
int x = 5;
int y = 10;
int z = 8;

// Expression returns r-value
int result = (x + y) * z;

// Expression returns l-value
++x = 6;

L-values and R-values play a crucial role in move semantics(to be described later). In C++, objects that are passed by value, such as in a function call, are often copied. This can be expensive for large objects, as copying a large object can take a lot of time. Or it is wasteful if the object created by copying is temporary. Move semantics allows for efficient transfers of large objects by transferring ownership of the resources instead of copying them.

R-value Reference

R-value references are a key component of move semantics in C++. An R-value reference is a special type of reference that can bind to an R-value, but not to an L-value. R-value references are denoted by two ampersands (&&).

 

R-value Reference

  • A reference created to a temporary
  • Represents a temporary
  • Created with && operator
  • Cannot point to l-values
  • R-value references always bind to temporaries
  • L-value references always bind to l-values

 

For example:

int&& rvalue = 5; // rvalue is an R-value reference
int&& rvalue2 = Add(5,8); // Add returns by value(temporary)
int&& rvalue3 = 7+2; // Expression return a temporary

int& lvalue = 5; // error, lvalue is an L-value reference and cannot bind to an R-value

R-value references are used in move constructors and move assignment operators to transfer ownership of the resources from one object to another. This allows for efficient transfers of large objects, as the ownership of the resources is transferred instead of copying them.

 

void Print(int& x) {
	std::cout << Print(int&)" << std::endl;
}

void Print(const int& x) {
	std::cout << Print(const int&)" << std::endl;
}

void Print(int&& x) {
	std::cout << "Print(int&&)" << std::endl;
}

int main(void) {
	int x = 10;
    Print(x);
    
    Print(3);
    return 0;
}

/*
Print(int&)
Print(int&&)

temporary will always bind to the function that accepts R-value reference
*/

Copy Semantics

Copy semantics refers to the process of creating a new object as a copy of an existing object. In C++, this can be achieved through the use of the copy constructor and the copy assignment operator.

 

The copy constructor is a constructor that takes an object of the same type as the target object as an argument and creates a new object by copying the contents of the argument object. The copy constructor is called when a new object is created from an existing object and can be defined as follows:

class MyClass {
public:
    MyClass(const MyClass& other) {
        // Copy constructor implementation
    }
};

The copy assignment operator is a special operator that is used to assign the contents of one object to another object of the same type. It can be defined as follows:

class MyClass {
public:
    MyClass& operator=(const MyClass& other) {
        // Copy assignment operator implementation
        return *this;
    }
};

In C++, it is important to define both the copy constructor and the copy assignment operator to ensure proper copy semantics. The default copy constructor and copy assignment operator provided by the compiler only perform a shallow copy, which may not be suitable for all classes.

#include <iostream>

class MyString {
public:
    // Constructor
    MyString(const char* str = "")
        : str_(new char[std::strlen(str) + 1]), length_(std::strlen(str)) {
        std::strcpy(str_, str);
        std::cout << "MyString constructor called" << std::endl;
    }

    // Copy constructor
    MyString(const MyString& other)
        : str_(new char[other.length_ + 1]), length_(other.length_) {
        std::strcpy(str_, other.str_);
        std::cout << "MyString copy constructor called" << std::endl;
    }

    // Destructor
    ~MyString() {
        std::cout << "MyString destructor called" << std::endl;
        delete[] str_;
    }

    // Get string
    const char* getString() const { return str_; }

    // Get length
    std::size_t getLength() const { return length_; }

private:
    char* str_;
    std::size_t length_;
};

int main() {
    MyString str1("Hello");
    std::cout << "str1: " << str1.getString() << " (" << str1.getLength() << ")" << std::endl;

    MyString str2 = str1;
    std::cout << "str2: " << str2.getString() << " (" << str2.getLength() << ")" << std::endl;

    return 0;
}

Move Semantics

Move semantics refers to the process of transferring ownership of resources from one object to another object. In C++, this can be achieved through the use of move constructors and move assignment operators.

 

The move constructor is a constructor that takes an object of the same type as the target object as an argument and creates a new object by transferring the resources of the argument object to the target object. The move constructor is called when a new object is created from an existing object and can be defined as follows:

class MyClass {
public:
    MyClass(MyClass&& other) {
        // Move constructor implementation
    }
};

The move assignment operator is a special operator that is used to transfer the ownership of resources from one object to another object of the same type. It can be defined as follows:

class MyClass {
public:
    MyClass& operator=(MyClass&& other) {
        // Move assignment operator implementation
        return *this;
    }
};

In C++, the use of move semantics is an effective way to improve the performance of resource-intensive operations by transferring ownership of resources instead of copying them.

It is important to note that after a move operation, the object being moved from is left in a valid but unspecified state. To force the compiler to use the move constructer, we can applay a type cast on this object and the type cast can be a static cast to an R-value reference. Or the std::move function(to be described later) can be used to trigger a move operation and transfer ownership of resources.

MyClass obj1;
auto obj2{static_cast<Integer&&>(a)};
auto obj3{std::move(a)};

In the above example, the std::move function is used to transfer ownership of the resources of obj1 to obj2. After the move, obj1 is left in a valid but unspecified state.

 

#include <iostream>
#include <utility>

class MyString {
public:
    // Constructor
    MyString(const char* str)
        : str_(str), length_(std::strlen(str)) {
        std::cout << "MyString constructor called" << std::endl;
    }

    // Move constructor
    MyString(MyString&& other)
        : str_(other.str_), length_(other.length_) {
        other.str_ = nullptr;
        other.length_ = 0;
        std::cout << "MyString move constructor called" << std::endl;
    }

    // Destructor
    ~MyString() {
        std::cout << "MyString destructor called" << std::endl;
        /*delete[] str_;*/
    }

    // Get string
    const char* getString() const { 
        if (str_ == nullptr)
            return "nullptr";
        else return str_;
    }

    // Get length
    std::size_t getLength() const { return length_; }

    void setString(const char* str="Test") {
        str_ = str;
    }

private:
    const char* str_;
    std::size_t length_;
};

int main() {
    MyString str1((char*)"Hello");
    std::cout << "str1: " << str1.getString() << " (" << str1.getLength() << ")" << std::endl;

    MyString str2 = std::move(str1);
    //str1.setString();
    std::cout << "str2: " << str2.getString() << " (" << str2.getLength() << ")" << std::endl;
    std::cout << "str1: " << str1.getString() << " (" << str1.getLength() << ")" << std::endl;

    return 0;
}

 

Rule of 5

The Rule of 5 is a guideline in C++ that states that if a class has a user-defined destructor, copy constructor, copy assignment operator, move constructor, or move assignment operator, then it should have all five. If a class does not need to manage its resources, such as a simple integer, the Rule of 0 can be used instead.

The five methods of the Rule of 5 are:

  • Copy Constructor: A constructor that takes an instance of the same class as an argument and creates a new instance by copying the argument.
  • Move Constructor: A constructor that takes an instance of the same class as an argument and creates a new instance by transferring ownership of the resources from the argument to the new instance.
  • Copy Assignment Operator: An operator that assigns an instance of the same class to another instance by copying the argument.
  • Move Assignment Operator: An operator that assigns an instance of the same class to another instance by transferring ownership of the resources from the argument to the target instance.
  • Destructor: A destructor is a special member function of a class that is executed whenever an instance of the class goes out of scope. It is used to free resources that are no longer needed.

For example, consider a class MyString that represents a string:

class MyString {
public:
    MyString(const char* str) : m_str(strdup(str)) {}
    ~MyString() { free(m_str); }
    MyString(const MyString& other) : m_str(strdup(other.m_str)) {}
    MyString& operator=(const MyString& other) {
        if (this != &other) {
            free(m_str);
            m_str = strdup(other.m_str);
        }
        return *this;
    }
    MyString(MyString&& other) : m_str(other.m_str) { other.m_str = nullptr; }
    MyString& operator=(MyString&& other) {
        if (this != &other) {
            free(m_str);
            m_str = other.m_str;
            other.m_str = nullptr;
        }
        return *this;
    }

private:
    char* m_str;
};

In the above example, the class MyString implements the Rule of 5, as it has a user-defined destructor, copy constructor, copy assignment operator, move constructor, and move assignment operator. The move constructor and move assignment operator allow for efficient transfers of MyString objects, as the ownership of the resources is transferred instead of copying them.

 

Custom Copy Constructor Copy Assignment Move Constructor Move Assignment Destructor
Copy constructor Custom =default =delete =delete =default
Copy assignment =default Custom =delete =delete =default
Move constructor =delete =delete Custom =delete =default
Move assignment =delete =delete =delete Custom =default
Destructor =default =default =delete =delete Custom
None =default =default =default =default =default

Copy Elision(Modification required)

Copy elision is an optimization in C++ that allows for the elimination of redundant copies. In some cases, such as returning a local object from a function, the compiler can optimize the code by eliminating the copy and returning the object directly. This optimization is also known as return value optimization.

For example:

MyString getMyString() {
    MyString str("hello");
    return str;
}

int main() {
    MyString str = getMyString();
    return 0;
}

In the above example, the function getMyString returns a MyString object, which would normally require a copy. However, due to copy elision, the compiler can optimize the code by eliminating the copy and returning the object directly.

Move Elision

Move elision is an optimization in C++ that allows for the elimination of redundant moves. In some cases, such as returning a local object from a function, the compiler can optimize the code by eliminating the move and returning the object directly. This optimization is similar to copy elision, but for move semantics.

For example:

MyString getMyString() {
    MyString str("hello");
    return str;
}

int main() {
    MyString str = std::move(getMyString());
    return 0;
}

In the above example, the function getMyString returns a MyString object, which would normally require a move. However, due to move elision, the compiler can optimize the code by eliminating the move and returning the object directly.

std::move

std::move is a function in the Standard Library that allows for the conversion of an L-value to an R-value. It is used in move semantics to transfer the ownership of resources from an L-value object to an R-value object. By using std::move, the object being moved from is left in a valid but unspecified state. This is because the ownership of the resources has been transferred to the R-value object.

For example:

MyString str1("hello");
MyString str2 = std::move(str1); // MyString str2{static_cast<MyString&&>(str1)};

In the above example, the std::move function is used to transfer the ownership of the resources from str1 to str2. After the move, str1 is left in a valid but unspecified state.

 

In conclusion, move semantics is a powerful feature in C++ that allows for efficient transfers of objects and their resources. Understanding the concepts of L-value, R-value, R-value reference, Rule of 5, Rule of 0, Copy Elision, Move Elision, and std::move is essential for writing efficient and correct C++ code.