[C++] C++11 – rvalue reference and move semantics, lvalue reference and rvalue reference, rvalue reference usage scenarios and meaning, perfect forwarding, new class functions

Article directory

  • C++11
    • 5. Rvalue references and move semantics
      • 5.1 lvalue references and rvalue references
      • 5.2 Comparison between lvalue reference and rvalue reference
      • 5.3 Usage scenarios and significance of rvalue references
      • 5.4 rvalue reference reference lvalue and some more in-depth analysis of usage scenarios
      • 5.5 Perfect forwarding
    • 6. New class functions

C++11

5. Rvalue reference and move semantics

Rvalue references are a new feature introduced in C++11 to support move semantics and perfect forwarding.

In C++, lvalues and rvalues are defined according to their position in an expression. An lvalue is an object on the left side of an assignment symbol in an expression, while an rvalue is an object on the right side of an assignment symbol. For example, in the expression a = b, a is an lvalue and b is an rvalue.

Before C++11, lvalues and rvalues were handled using ordinary references (T & amp;). However, this can cause some inconveniences when handling certain situations, such as when implementing move constructors and move assignment operators.

To solve this problem,C++11 introduces the concept of rvalue reference (T&&). An rvalue reference is a special reference type that can only be bound to an rvalue. This allows us to implement move semantics, i.e. moving resources from one object to another instead of doing a deep copy.

Rvalue references can also be used to achieve perfect forwarding, that is, passing parameters to other functions as they are in function templates, keeping their original lvalue or rvalue properties unchanged. This needs to be achieved using the std::forward function.

5.1 lvalue reference and rvalue reference

What is an lvalue and what is an rvalue?

Lvalues and rvalues are defined according to their position in the expression.

An lvalue is an object that can be on the left side of an assignment symbol and represents the identity of an object (for example, a variable with a name). The lvalue must have an entity in memory, and its address can be obtained using the address operator & amp;.

Rvalue refers to the object located on the right side of the assignment symbol, which represents the value of an object. An rvalue can be a temporary object (temporary variable) or a value in memory or a CPU register. When an object is used as an rvalue, its content (value) is used, not its identity.

Simply put, the difference between lvalue and rvalue is: lvalue represents the identity of an object, while rvalue represents the value of an object. An lvalue can take an address, but an rvalue cannot take an address.

Lvalue example:

int a = 10; // a is an lvalue because it is a variable with a name and can be used on the left side of the assignment symbol
int b = a; // a is an lvalue because it is on the left side of the assignment symbol

Rvalue example:

In this example, the result of a + b is a temporary value with no specific identity (variable name) and can only be used on the right side of the assignment symbol, so it is an rvalue.

int a = 10;
int b = 20;
int c = a + b; // The result of a + b is an rvalue because it is a temporary value and has no specific identity (variable name)

What is an lvalue reference? What is an rvalue reference?

Lvalue reference (lvalue reference) is a type that refers to an lvalue. Specifically, it is an alias for a variable with a name through which the variable’s value can be accessed and modified. An lvalue reference is represented by T & amp;, where T is the type of the variable being referenced.

Rvalue reference (rvalue reference) is a type that refers to an rvalue. Since rvalues usually do not have names, we can only find their existence through references. An rvalue reference is represented by T & amp; & amp;, where T is the type of the variable being referenced.

Example of lvalue reference:

An lvalue is an expression that represents data (such as a variable name or a dereferenced pointer).We can get its address + we can assign a value to it. The lvalue can appear on the left side of the assignment symbol, but the rvalue cannot appear. To the left of the assignment symbol. When defined, the lvalue after the const modifier cannot be assigned a value, but its address can be taken. An lvalue reference is a reference to an lvalue, and an alias is given to the lvalue.

int main()
{<!-- -->
// The following p, b, c, *p are all lvalues
int* p = new int(0);
int b = 1;
const int c = 2;
\t
//The following are lvalue references to the above lvalues
int* & amp; rp = p;
int & rb = b;
const int & rc = c;
int & amp; pvalue = *p;
return 0;
}

Rvalue reference example:

Rvalue is also an expression that represents data, such as: literal constant, expression return value, function return value (this cannot be an lvalue reference return), etc.Rvalue can appear on the right side of the assignment symbol, But it cannot appear on the left side of the assignment symbol, and the rvalue cannot take an address. An rvalue reference is a reference to an rvalue, giving an alias to the rvalue.

int main()
{<!-- -->
double x = 1.1, y = 2.2;
\t
//The following are common rvalues
10;
x + y;
fmin(x, y);
\t
//The following are all rvalue references to rvalues
int & amp; & amp; rr1 = 10;
double & amp; & amp; rr2 = x + y;
double & amp; & amp; rr3 = fmin(x, y);
\t
// An error will be reported when compiling here: error C2106: "=": The left operand must be an lvalue
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}

5.2 Comparison between lvalue references and rvalue references

Lvalue reference summary:

(1) An lvalue reference can only reference lvalues, not rvalues.

(2) But const lvalue reference can refer to both lvalue and rvalue

int main()
{<!-- -->
    // An lvalue reference can only refer to an lvalue, not an rvalue.
    int a = 10;
    int & amp; ra1 = a; // ra is an alias for a
    //int & amp; ra2 = 10; // Compilation fails because 10 is an rvalue

    // A const lvalue reference can refer to both an lvalue and an rvalue.
    const int & ra3 = 10;
    const int & ra4 = a;
    return 0;
}

Rvalue reference summary:

(1) Rvalue references can only refer to rvalues, not lvalues.

(2) But rvalue references can move later lvalues.

int main()
{<!-- -->
// Rvalue references can only refer to rvalues, not lvalues.
int & amp; & amp; r1 = 10;
\t
// error C2440: 'initialization': cannot convert from 'int' to 'int & amp; & amp;'
// message: Cannot bind lvalue to rvalue reference
int a = 10;
int & amp; & amp; r2 = a;
\t
// Rvalue references can refer to lvalues after move
int & amp; & amp; r3 = std::move(a);
return 0;
}

5.3 Usage scenarios and significance of rvalue references

Scenarios for lvalue references:

(1) Parameters:

When a function needs to pass large objects or complex data structures, you can use lvalue references for parameter passing to avoid object copying. This can improve the execution efficiency of the function and reduce memory usage. For example:

In this example, ComplexObject is a complex data structure, and using lvalue references for parameter passing can avoid object copying.

void foo(const ComplexObject & amp; obj) {<!-- -->
    // ...
}

(2) Return value:

When a function needs to return a large object or complex data structure, you can use an lvalue reference as the return value to avoid copying the object. This can improve the execution efficiency of the function and reduce memory usage. For example:

In this example, the bar function returns an lvalue reference of type ComplexObject to avoid copying the object. Since a reference is returned, the caller has direct access to the original object rather than a copied copy.

ComplexObject & amp; bar() {<!-- -->
    static ComplexObject obj;
    return obj;
}

In summary, whether they are parameters or return values, they can reduce copying and improve efficiency.

Take the implementation of String as an example. lvalue references are used to implement the copy constructor and assignment overloaded operator of the string class.

In the copy constructor, an lvalue reference is used to receive an incoming string object (const string & s), thus avoiding copying of the object. By creating a temporary object (string tmp(s._str)), and then using the swap function to exchange the resources of the temporary object to the current object, the effect of deep copy is achieved. The lvalue reference here is passed as a parameter, allowing us to directly access the member variables of the passed in string object.

In the assignment overloaded operator, lvalue reference is also used to receive an incoming string object (const string & s). The function of the lvalue reference here is to allow us to assign the returned object to the current object. By creating a temporary object (string tmp(s)), and then using the swap function to swap the resources of the temporary object to the current object, the effect of deep copy is achieved. Finally, a reference to the current object is returned (return *this) so that assignment operations can be performed continuously.

To sum up, in this code snippet, lvalue reference is used to implement the copy constructor and assignment overload operator of deep copy.It allows us to directly access the passed in object and avoid the copying of the object. Improved program performance.

// s1.swap(s2)
void swap(string & s)
{<!-- -->
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}

// copy construction
string(const string & s)
:_str(nullptr)
{<!-- -->
cout << "string(const string & amp; s) -- deep copy" << endl;
string tmp(s._str);
swap(tmp);
}

// Assignment overloading
string & amp; operator=(const string & amp; s)
{<!-- -->
cout << "string & amp; operator=(string s) -- deep copy" << endl;
string tmp(s);
swap(tmp);
return *this;
}

Shortcomings of lvalue references:

But when the function return object is a local variable, it does not exist outside the function scope, so you cannot use lvalue reference to return, you can only return by value.

The following is a code example that demonstrates the shortcomings of lvalue references:

class String {<!-- -->
public:
    String(const char* str = "")
        : _str(nullptr)
    {<!-- -->
        std::cout << "String(const char* str) -- deep copy" << std::endl;
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }
  
    String(const String & amp; other)
        : _str(nullptr)
    {<!-- -->
        std::cout << "String(const String & amp; other) -- deep copy" << std::endl;
        _str = new char[strlen(other._str) + 1];
        strcpy(_str, other._str);
    }
  
    String & amp; operator=(const String & amp; other)
    {<!-- -->
        std::cout << "String & amp; operator=(const String & amp; other) -- deep copy" << std::endl;
        if (this != & amp;other) {<!-- -->
            delete[] _str;
            _str = new char[strlen(other._str) + 1];
            strcpy(_str, other._str);
        }
        return *this;
    }
  
    ~String()
    {<!-- -->
        delete[] _str;
    }
  
private:
    char* _str;
};
  
void printString(const String & str)
{<!-- -->
    std::cout << "printString(const String & amp; str)" << std::endl;
    std::cout << str._str << std::endl;
}
  
int main()
{<!-- -->
    String s1("Hello");
    String s2 = "World";
    printString(s1 + s2); // A temporary object will be created here, which cannot be captured with an lvalue reference
    return 0;
}

In the above code, we define a simple String class and implement the copy constructor, assignment overloaded operator and destructor. In the main function, we create two String objects s1 and s2, and then call the printString function to print their concatenated results.

However, in the printString function, we try to capture the incoming string object through an lvalue reference, but the incoming parameter here is actually a temporary object (created by s1 + s2) and cannot be captured with an lvalue reference . This is one of the shortcomings of lvalue references. It cannot be bound to temporary objects or rvalues, preventing us from directly accessing the resources of these objects. To solve this problem, C++11 introduced the concept of rvalue references.

Rvalue references and move semantics solve the above problems:

Using rvalue references and move semantics can solve the above problems. The specific method is to implement the move constructor and move assignment operator in the String class, use rvalue references to capture the temporary object, and move its resources to the current object to avoid deep copying.

The following is a modified code example:

We implemented the move constructor and move assignment operator in the String class, using rvalue references to capture the temporary object and move its resources into the current object. In the main function, the temporary object can be captured using an rvalue reference so that the move constructor and move assignment operator can be called. In this way, deep copies can be avoided and the performance of the program can be improved.

class String {<!-- -->
public:
    String(const char* str = "")
        : _str(nullptr)
    {<!-- -->
        std::cout << "String(const char* str) -- deep copy" << std::endl;
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }
  
    String(const String & amp; other)
        : _str(nullptr)
    {<!-- -->
        std::cout << "String(const String & amp; other) -- deep copy" << std::endl;
        _str = new char[strlen(other._str) + 1];
        strcpy(_str, other._str);
    }
  
    String(String & amp; & amp; other)
        : _str(nullptr)
    {<!-- -->
        std::cout << "String(String & amp; & amp; other) -- move construction" << std::endl;
        _str = other._str;
        other._str = nullptr;
    }
  
    String & amp; operator=(const String & amp; other)
    {<!-- -->
        std::cout << "String & amp; operator=(const String & amp; other) -- deep copy" << std::endl;
        if (this != & amp;other) {<!-- -->
            delete[] _str;
            _str = new char[strlen(other._str) + 1];
            strcpy(_str, other._str);
        }
        return *this;
    }
  
    String & amp; operator=(String & amp; & amp; other)
    {<!-- -->
        std::cout << "String & amp; operator=(String & amp; & amp; other) -- move assignment" << std::endl;
        if (this != & amp;other) {<!-- -->
            delete[] _str;
            _str = other._str;
            other._str = nullptr;
        }
        return *this;
    }
  
    ~String()
    {<!-- -->
        delete[] _str;
    }
  
private:
    char* _str;
};
  
void printString(const String & str)
{<!-- -->
    std::cout << "printString(const String & amp; str)" << std::endl;
    std::cout << str._str << std::endl;
}
  
int main()
{<!-- -->
    String s1("Hello");
    String s2 = "World";
    printString(s1 + s2); // Temporary objects can be captured using rvalue references
    return 0;
}

5.4 Rvalue reference refers to lvalue and some more in-depth analysis of usage scenarios

According to the syntax, rvalue references can only reference rvalues, but rvalue references must not reference lvalues?

But in some scenarios, you may really need to use rvalues to reference lvalues to achieve move semantics. When you need to use an rvalue reference to refer to an lvalue,you can convert the lvalue into an rvalue through the move function. In C++11, the std::move() function is located in the header file. This function can coerce an lvalue into an rvalue reference and then implement move semantics.

template<class _Ty>
inline typename remove_reference<_Ty>::type & amp; & amp; move(_Ty & amp; & amp; _Arg) _NOEXCEPT
{<!-- -->
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type & amp; & amp;)_Arg);
}

int main()
{<!-- -->
bit::string s1("hello world");
// Here s1 is an lvalue, and the copy constructor is called.
\t
bit::string s2(s1);
// Here, after we process s1 move, it will be treated as an rvalue and the move constructor will be called.
// But please note here, generally do not use it like this, because we will find that s1
// The resource is transferred to s3 and s1 is made empty.
bit::string s3(std::move(s1));
return 0;
}

Move function usage example:

void push_back (value_type & amp; & amp; val);
int main()
{<!-- -->
list<bit::string> lt;
bit::string s1("1111");
// What is called here is the copy constructor
lt.push_back(s1);
\t
//The following calls are all move constructors
lt.push_back("2222");
lt.push_back(std::move(s1));
return 0;
}

// string(const string & amp; s) -- deep copy
// string(string & amp; & amp; s) -- move semantics
// string(string & amp; & amp; s) -- move semantics

5.5 perfect forwarding

Perfect Forwarding is a feature introduced in C++11. It allows function templates to forward parameters to other functions according to their original form (type, value category), thereby achieving more flexible programming. .

The core of perfect forwarding is to use the std::forward function template, which can convert lvalues into lvalue references and rvalues into rvalue references, thereby achieving perfect forwarding of parameters. Here’s a simple example:

template<typename F, typename T1, typename T2>
void flip(F f, T1 & amp; & amp; t1, T2 & amp; & amp; t2)
{<!-- -->
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

In this example, the flip function template takes a function object f and two parameters t1 and t2, and forwards them to the function object f, but in the reverse order of the original order. By using std::forward, we can ensure that the type and value category of the parameters are correctly preserved and forwarded.

Perfect forwarding is very useful in many situations, such as when implementing proxy functions, factory functions, callback functions, etc. It allows us to handle parameters more flexibly and avoid unnecessary copy or move operations.

Perfect forwarding retains the object’s native type attributes during the parameter passing process:

void Fun(int & amp;x){<!-- --> cout << "lvalue reference" << endl; }
void Fun(const int & amp;x){<!-- --> cout << "const lvalue reference" << endl; }
void Fun(int & amp; & amp;x){<!-- --> cout << "rvalue reference" << endl; }
void Fun(const int & amp; & amp;x){<!-- --> cout << "const rvalue reference" << endl; }

// std::forward<T>(t) maintains the native type attributes of t during the parameter passing process.
template<typename T>
void PerfectForward(T & amp; & amp; t)
{<!-- -->
Fun(std::forward<T>(t));
}

int main()
{<!-- -->
PerfectForward(10); // rvalue
\t
int a;
PerfectForward(a); // lvalue
PerfectForward(std::move(a)); // rvalue
\t
const int b = 8;
PerfectForward(b); // const lvalue
PerfectForward(std::move(b)); // const rvalue
return 0;
}

Example of a perfect forward:

void print(int & amp; & amp; i) {<!-- -->
    std::cout << "rvalue: " << i << std::endl;
}
  
void print(const int & amp; i) {<!-- -->
    std::cout << "lvalue: " << i << std::endl;
}
  
template<typename T>
void forwardPrint(T & amp; & amp; t) {<!-- -->
    print(std::forward<T>(t));
}
  
int main() {<!-- -->
    int i = 42;
    forwardPrint(i); // lvalue: 42
    forwardPrint(std::move(i)); // rvalue: 42
    return 0;
}

6. New class functions

Default member function:

It turns out that there are 6 default member functions in the C++ class:

(1) Constructor(2) Destructor

(3) Copy constructor(4) Copy assignment overloading

(5) Overloading by taking address?(6) Overloading by taking const address

C++ 11 adds two new ones: move constructor and move assignment operator overloading.

There are some points that need to be noted regarding the overloading of move constructors and move assignment operators as follows:

(1) If you have not implemented the move constructor yourself, and have not implemented any of the destructor, copy construction, and copy assignment overloading. Then the compiler will automatically generate a default move constructor. The move constructor generated by default will perform member-by-member and byte-by-byte copying for built-in type members. For custom type members, you need to see whether the member implements the move constructor. If it does, call the move constructor. If it does not, then the move constructor will be called. Call copy construction.

(2) If you do not implement the move assignment overloading function yourself, and do not implement any of the destructor, copy construction, and copy assignment overloading, the compiler will automatically generate a default move assignment. The move constructor generated by default will perform member-by-member and byte-by-byte copying for built-in type members. For custom type members, you need to see whether the member implements move assignment. If it does, call the move assignment. If it does not, then the move constructor will be called. Call copy assignment. (The default move assignment is completely similar to the move construction above)

(3) If you provide move construction or move assignment, the compiler will not automatically provide copy construction and copy assignment.

The keyword default: that forces the generation of the default function:

C++11 gives you more control over the default functions to be used. Suppose you want to use a default function, but for some reason this function is not generated by default. For example: if we provide a copy constructor, the move constructor will not be generated. Then we can use the default keyword to display the specified move constructor generation.

class Person
{<!-- -->
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{<!-- -->}
\t
Person(const Person & p)
:_name(p._name)
,_age(p._age)
{<!-- -->}
\t
Person(Person & amp; & amp; p) = default;
\t
private:
bit::string _name;
int _age;
};

int main()
{<!-- -->
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}

The keyword delete: that prohibits generating default functions is

If you want to limit the generation of certain default functions, in C++98, set the function to private and only declare the patch, so that an error will be reported as long as others want to call it. It is simpler in C++11. Just add =delete to the function declaration. This syntax instructs the compiler not to generate a default version of the corresponding function. The function modified with =delete is called a delete function.

class Person
{<!-- -->
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{<!-- -->}
\t
Person(const Person & amp; p) = delete;
\t
private:
bit::string _name;
int _age;
};

int main()
{<!-- -->
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}