preface

I changed my job some time ago, and the technology stack gradually shifted from Android to cross-end development. I need to pick up C++ again, so I plan to summarize it into a series.

This series will summarize the new features of C++11 from a practical point of view and try to keep them simple and easy to understand.

In the face of the crowd, there are students who have C++ foundation and want to quickly understand the new features of modern C++. I feel that there are many students, since most universities teach traditional C++.

Without further further, this article will take a look at the concept of Rvalue reference and its application scenario movement semantics.

The origin of the Rvalue reference

Prior to C++11, all references were lvalue references, that is, references to lvalues. Lvalues, usually placed to the left of an assignment expression, are named objects allocated on the heap or stack that have specific memory addresses.

The lvalue’s other friend, rValue, on the right side of the assignment expression, has no identifiable memory address. In hardware terms, rvalues only exist in temporary registers.

Take the following code for example:

    int a = 1;
    int& b = a;
Copy the code

Obviously, here a is an lvalue, 1 is an Rvalue, and b is an lvalue reference, which is an alias for a.

Or take this:

    int& a = 1;
Copy the code

Non-const lvalue reference to type ‘int’ cannot bind to a temporary of type ‘int’

No one would make this mistake, but we can also use constant lvalue references to point to rvalues, like this:

    const int& a = 1; // Constant lvalue reference
Copy the code

The question is, why can a constant lvalue reference point to an Rvalue?

Because the const value cannot be modified, it can be understood that a temporary value is generated internally and can be retrieved from the address. Something like the following:

    const int tmp = 1; 
    const int &a = tmp;
Copy the code

As you can see, const Type& is a common custom in C++. Function arguments refer to const Type& as a constant to avoid creating unnecessary temporary objects:

    void func(const std::string& a);
    func("hello");	
Copy the code

The downside of this approach is that you can’t change the const constant.

The introduction of C++11’s new friend, rvalue references, solves this problem to some extent.

The Rvalue reference, Type&&, is used to point to an Rvalue and can be modified.

    void func(const std::string&& a){
        a = "world"; // Modify the rvalue
    }
    func("hello");	
Copy the code

OK, so here’s a quick summary

  • The key difference is that lvalues are addressable and Rvalues are not;
  • Using left – or – value references for function passes can avoid copying, but rvalue references are more flexible.

So, what are the specific application scenarios for rvalue references?

Mobile semantics improve performance

An important function of Rvalue references is to support mobility semantics. The copy semantics may be easier to understand than the move semantics.

For example, we can define a copy constructor to implement a deep copy of an object. If not defined, the compiler will have a default implementation, which is a shallow copy.

class Stack
{
public:
    Stack(int size = 100) : size_(size)
    {
        cout << "Constructor" << endl;
        stack_ = new int[size];
    }
    Stack(const Stack &src):size_(src.size_)
    {
        cout << "Copy constructor" << endl;
        stack_ = new int[src.size_];

        / / copy
        for (int i = 0; i < size_; ++i) { stack_[i] = src.stack_[i]; }} -Stack()
    {
        cout << "Destructor" << endl;
        delete[] stack_;
        stack_ = nullptr;
    }

private:
    int size_;
    int *stack_;
};

int main(a)
{
    Stack stack(10);
    Stack stack2 = stack;
}

Copy the code

The run output is:

Constructor copy constructor destructor DestructorCopy the code

In addition, in some scenarios, such as when the copied person is no longer needed, we can actually use STD ::move to trigger the move semantics to avoid deep copies and improve performance.

So in the above code, we can add a move constructor, which is widely used in STL and custom classes.

Stack(Stack&& src):size_(src.size_) {
    cout << "Move constructor" << endl;
    stack_ = src.stack_;
    src.stack_ = nullptr;
}

int main(a){
    Stack stack(10);
    //Stack stack2 = stack; // Take the copy construct
    Stack stack2 = std::move(stack); // Move the structure
}
Copy the code

The output from the run is:

Constructor move constructor destructor destructorCopy the code

Here, STD ::move converts an Lvalue to an Rvalue reference, and the move constructor transfers ownership of the passed object to the current object, then hollowing-out the passed object.

STD: : move

You might think STD ::move does some magic, but it doesn’t. It’s just a cast. The real move happens in the move constructor or the move assignment operator. Take a look at the code

  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

Copy the code

The discovery is just a static_cast cast, where the effect of remove_Reference is to remove the reference from T, whether it is an Lvalue or an Rvalue, and get only the type in it. Just to simplify, when _TP is a string, this function is essentially a string

string&& move(string&& __t)
{
    return static_cast<string&&>(__t);
}
Copy the code

Therefore, no matter whether the passed parameter is an Lvalue or an Rvalue, the final return must be an Rvalue reference.

In fact, the STD ::move runtime doesn’t do anything, because no executable code is generated after compilation, but the internal pass-through of variable addresses can be optimized away. Interested students can take a look at a brief analysis of C++ rvalue references through assembler

Finally, if you’re smart enough to notice that the STD ::move function argument && looks like an Rvalue reference type, can an Lvalue be passed in?