A simple understanding summary of C++11's right value reference, mobile semantics and perfect forwarding

1 right value reference

  • Function return procedure:
    The life cycle of a non static temporary variable in a function is at the end of the function call, so there will be a contradiction. It can be inferred how to return an object that will disappear when it comes out of the function body. When the function returns the object, it will make a copy, that is, the upper function copies an object returned by the lower function, Then the temporary object in the lower function will be destructed, and the new object copied from the upper function will continue to be used.

  • for instance:

	class A 
	{
	  public:
	    int a;
	};
	A getTemp()
	{
	    return A();
	}
	int main()
	{
		A maina = getTemp();
	}

Call steps of the above code:

  • Construction steps:
  • 1.getTemp function calls A's constructor to construct A temporary object
  • 2. When constructing a temporary object and returning it to the getTemp function, copy it as a temporary object in getTemp().
  • 3. When returning from getTemp(), copy another copy of the temporary object in the function to the maina object in the main() function (this is also a temporary object, but it has a name)
  • Deconstruction steps:
  • 1. The constructor of A called at the beginning returns to getTemp() and is destructed.
  • 2. The temporary object of getTemp() will be destructed after it is out of the scope of getTemp().
  • 3.maina will also be destructed after it is out of the scope of the main() function.
    Therefore, it can be seen that the above code calls the constructor once, copies it twice, and A destructs it three times.

If the object is large, the construction and destruction of temporary objects will cost a lot for the program, and the generation and destruction of temporary variables have no feeling for the programmer, because it does not affect the correctness of the program. It will only secretly affect the performance of your program. (really bad)

C++11 optimizes this, adds a mobile constructor, and gives the temporary object generated by getTemp() of the above code directly to the main() function of the upper layer, instead of making another copy of the main() function. Then the temporary object received in the main () function is given the name maina.
This reduces the need for temporary objects to be copied and constructed continuously, and then destructed.

The move constructor parameter of C++11 is an R-value reference. Let's first understand what is an l-value and R-value.

  • Distinguish between the left and right values depends on whether the expression can use the & sign to take the address. Those that can take the address as the left value and those that cannot take the address as the right value.
		int i = 0;// i is the left value and 0 is the right value
		&i; // correct
		&0; // error

An R-value reference can be understood as a reference to a temporary object. A temporary object is something that a programmer cannot intuitively see in the code and feel the construction and Deconstruction of a temporary object. Adding a reference to a temporary object is equivalent to adding a name to the memory of the temporary object.
For example: A & & A = getTemp(); Is to add a name a to the temporary object in getTemp();

Differences between references:
Lvalue references can only bind lvalues
Right value references can only bind right values
However, constant lvalue reference is a universal reference, which can bind non constant lvalues, constant lvalues and constant lvalues. It can only be read and cannot be modified after binding

 int a = 0;
 int &b = 1; // incorrect
 int &&c = a; // incorrect
 const int d = 1;
 const int& e = a; //sure
 const int& f = 1; // sure
 const int& g = d; // sure

2. Mobile structure

There are several construction related functions in C + + classes: default constructor, ordinary constructor, copy constructor and mobile constructor
Copy assignment function, move assignment function (overload operator "=")

class A
{
public:
    // Default constructor 
    A() 
    {
        str_ = new char[1];
        *str_ = '\0';
    };
    //Ordinary constructor
    A(const char* cstr)
    {
        if (cstr)
        {
            str_ = new char[strlen(cstr) + 1];
            strcpy(str_, cstr);
        }
        else
        {
            str_ = new char[1];
            *str_ = '\0';
        }
    };
     //copy constructor 
    A(const A& a)
    {
        str_ = new char[strlen(a.str_) + 1];
        strcpy(str_, a.str_);
    }
     //Copy assignment function
    A& operator = (const A& a)
    {
        if (this == &a)
            return *this;
        delete[] str_;
        str_ = new char[strlen(a.str_) + 1];
        strcpy(str_, a.str_);
        return *this;
    }
    //move constructor 
    A(A&& a)noexcept
    {
        str_ = a.str_;
        a.str_ = nullptr;
    }
    // Move assignment function
    A& operator = (A&& a) noexcept
    {
        if (this == &a)
            return *this;
        delete[] str_;
        str_ = a.str_;
        this->str_ = a.str_;
        return *this;
    }
    ~A()
    {
        delete[] str_;
    }
    char* get_str() const { return str_; }
private:
    char* str_;
};
  • The difference between the move constructor and the copy constructor:
    The parameter of copy construction is const A &, which is a constant lvalue reference, while the parameter of move construction is a & & which is an lvalue reference. When the temporary object is an lvalue, it is preferred to enter the move constructor instead of the copy constructor.
    For example, the following code
	vector<A> vc;
    for (int i = 0; i < 100; i++) 
    {
        vc.push_back(A("aaa"));
    }

The "aaa" is a right value. After calling the constructor to construct a temporary object, the mobile constructor is called to move the temporary value constructed to vector instead of copying one copy to vector.

If you write as follows:

	A s("aaa");
    for (int i = 0; i < 100; i++) 
    {
        vc.push_back(s);
    }

If s is an lvalue, the copy constructor will be called, a copy will be copied into the vector, and then the temporary value will be destructed.

Note: the move constructor needs to empty the original pointer before the move, otherwise the moved data will be destructed when destructing..

So if you want to use lvalues, you can also call the move constructor to reduce copies. What should you do?
The mobile semantic std::move provided by C++11 can solve the above problems

3. Mobile semantic std::move

The only function of std::move is to cast an lvalue into an lvalue
In this way, we can call the mobile constructor instead of the copy constructor.

	A s("aaa");
    for (int i = 0; i < 100; i++) 
    {
        vc.push_back(std::move(s));
    }
  • Note: we use mobile semantics to call a mobile constructor to grab other object resources. What happens to the robbed object
    A s("aaa");
    A a(std::move(s));
    std::cout << s.get_str() << std::endl;

In the above code, s calls the ordinary constructor to construct itself, and then a uses the move semantics to force the left value of s into the right value to call the mobile constructor. Then s itself has failed. If you call the pointer of s to obtain data, there will be an error, because the original pointer has been set empty in the constructor.

4. Distinguish between shallow copy, deep copy and mobile semantics

Shallow copy: copy only the pointer
Note: it may cause repeated deconstruction of a resource

Deep copy: copy memory, and the new pointer points to the new memory

Mobile semantics: grab memory resources for your own use
Note: the original pointer is useless

5. Perfect forwarding std::forward

  • Definition: in function templates, the parameters are passed to another function called in the function template according to the parameter type of the template (the left and right value attribute of the reserved parameter).
template<typename T>
void func(T & val)
{
	print(val);
}

For example, for the above function template, pass in 1 and follow func(1); Call 1 is an R-value, but after entering func, if 1 becomes an l-value val, it will become a call print(val); val is an lvalue.
If I want to call print(1) instead of print(val) when calling print function, I can use std::forward function for perfect forwarding.
It can be written as follows:

template<typename T>
// If you write T &, you do not support the incoming right value, while writing T &, you can support both left and right values
void func(T&& val)
{
    print(std::forward<T>(val));
}

Keywords: C++ Programming Visual Studio

Added by uniflare on Sat, 05 Mar 2022 16:16:59 +0200