1. Rvalue references

Before C++11, rvalues were considered useless resources, so rvalue references were introduced in C++11 in order to reuse rvalues. To define an rvalue reference, use &&. An rvalue reference must not be initialized by an lvalue, only by an rvalue. So why? Because the purpose of an rvalue reference is to extend the lifetime of the object used to initialize it, you do not need to extend the lifetime of an lvalue depending on its scope. Since it is an extension, the following situation occurs:

int x = 20;   / / the left value
int&& rx = x * 2;  // the result of x*2 is an rvalue, and rx extends its lifetime
int y = rx + 2;   // So you can reuse it: 42
rx = 100;         // Once you initialize an rvalue reference variable, the variable becomes an lvalue that can be assigned
Copy the code

1.1. Rvalue reference

1.1.1. Life cycle extension

Before C++11, we used to write examples like this:

class A
{
public:
    A() : m_ptr(new int(0))
    {
        cout << "construct" << endl;
    }
    A(const A& rhs) : m_ptr(new int(*rhs.m_ptr)) // Copy the constructor
    {
        cout << "copy construct" << endl;
    }
    ~A()
    {
        cout << "destruct" << endl;
        delete m_ptr;
    }
private:
    int* m_ptr;
};

A Get(bool flag)
{
    A a, b;
    if (flag)
    {
        return a;
    }
    return b;
}

int main(a)
{
    A a = Get(false);// Call the copy constructor
    return 0; } execute result: construct construct copy construct destruct destruct destructCopy the code

Problem: in the above example the Get function returns a temporary variable, and then copy through this temporary variables to construct a new object, a temporary variable after the completion of a copy to construct is destroyed, if the heap memory is very big, this copy is time consuming of construction, bring the loss of performance, if there is a way to avoid a copy of the temporary object structure? That is, change to the following form:

class A
{
public:
    A() : m_ptr(new int(0))
    {
        cout << "construct" << endl;
    }
    A(const A& rhs) : m_ptr(new int(*rhs.m_ptr)) // Copy the constructor
    {
        cout << "copy construct" << endl;
    }
    A(A&& rhs) : m_ptr(rhs.m_ptr) // Move the constructor
	{
  		rhs.m_ptr = nullptr;
 		 cout << "move construct" << endl;
	}
    ~A()
    {
        cout << "destruct" << endl;
        delete m_ptr;
    }
private:
    int* m_ptr;
};

A Get(bool flag)
{
    A a, b;
    if (flag)
    {
        return a;
    }
    return b;
}

int main(a)
{
    A a = Get(false);// Call the copy constructor
    return 0; } execute result: construct construct move construct destruct destructCopy the code

Instead of calling the copy constructor, move Construct is called. As can be seen from the implementation of the move constructor, its parameter is an rvalue reference type parameter T&&, there is no deep copy, only shallow copy, so as to avoid the deep copy of temporary objects, and the repeated application and release of resources, improve performance; Extended the life of the temporary object, i.e. in the above example, the life of the return value temporary object is assigned to the a object in main.

1.1.2. Transferring non-copiable Resources

If the resource is non-copyable, then the object loaded with the resource should also be non-copyable. If the resource object is not copyable, you generally need to define the move constructor/move assignment function and disable the copy constructor/copy assignment function. For example, the smart pointer STD ::unique_ptr can only move:

template<typename T>
class unique_ptr {
 public:
  unique_ptr(const unique_ptr& rhs) = delete;
  unique_ptr(unique_ptr&& rhs) noexcept;  // move only
 private:
  T* data_ = nullptr;
};

unique_ptr::unique_ptr(unique_ptr&& rhs) noexcept {
  auto &lhs = *this;
  lhs.data_ = rhs.data_;
  rhs.data_ = nullptr;
}
Copy the code

1.2. Rvalues reference a few notes

  • An rvalue reference is a type that references an rvalue. Because an rvalue is not named, it can only be found by reference.
  • By declaring an rvalue reference, the rvalue is “reborn”, with the same life cycle as an rvalue reference type variable.
  • Whether declared as an rvalue reference or an lvalue reference must be initialized immediately;

1.3. Rvalue references solve the following problems

1.3.1. Move semantics STD :: Move

Moving semantics match temporary values through rvalue references. Can ordinary lvalues also be optimized with moving semantics?

  • C++11In order to solve the above problems, thestd::moveMethod to convert an lvalue to an rvalue to facilitate the application of mobile semantics, that is, whether you pass it an lvalue or an rvalue, throughstd::moveAnd then they all become rvalues;
  • moveIt doesn’t actually move anything, its only function is to cast an lvalue to an rvalue reference so that we can use that value through an rvalue reference;
 class string {
 public:
  string(const string& other);  // Copy constructor, exists pre C++11

  string(string&& other) {      // Move constructor, new in C++11
    length = other.length;
    capacity = other.capacity;
    data = other.data;
    other.data = nullptr; // Set pointer to temporary object to null, other.data = nullptr; . If you do not set other.data to null, other.data will be freed twice, resulting in a program exception.
  }

 private:
  size_t length;
  size_t capacity;
  const char* data;
};

int main(a){
  string a(get_string());  // move constructor
  string b(a);             // copy constructor
  string c(std::move(b));  // move constructor
  return 0;
}
Copy the code

* * note: String c(STD ::move(b)) will still call the copy constructor if there is no move constructor string(string&& other) in string. Because the argument to string(const string& other) is const reference.

C + + 11, the compiler will automatically added in the class move constructor and assignment operator, so the c + + 11, declare a class, the compiler will add the default constructors, copy constructors, destructors, move constructor and the assignment operation, the so-called “five carriages” in the class, rather than the c + + 11 before “three carriages”.

If the user defines the copy constructor, the compiler will not automatically add the move constructor and move assignment. You can add = delete after these functions to explicitly prevent the compiler from automatically adding the corresponding function.

After STD ::move transfers an lvalue to an rvalue, the original variable loses ownership of memory, and subsequent code cannot use the variable to access the object. If used, undefined behavior occurs.

std::string base_url = tag->GetBaseUrl(a);if(! base_url.empty()) {
  UpdateQueryUrl(std::move(base_url) + "&q=" + word_); // If the base_URL command is executed, the base_URL will not be used later, and undefined behavior will occur if it is used.
}
LOG(INFO) << base_url;  // |base_url| may be moved-from

Copy the code

The object being moved is in a legal but unspecified state:

  • Basic requirements) correctly destruct (does not repeatedly release resources that have been moved, for examplestd::unique_ptr::~unique_ptr() Check whether the pointer is neededdelete)
  • (General requirements) After reassignment, it is the same as the new object (C++ The library is based on this assumption.)
  • (Higher requirements) restore to default values (e.gstd::unique_ptr Revert tonullptr)
auto p = std::make_unique<int> (1);
auto q = std::move(p);

assert(p == nullptr);  // OK: reset to default
p.reset(new int{2});   // or p = std::make_unique<int>(2);
assert(*p == 2);       // OK: reset to int*(2)
Copy the code

Because a base type does not contain resources, it is moved the same as a copy: once moved, the original value remains.

int i = 1;
int j = std::move(i);

assert(i == j)
Copy the code

1.3.2. Perfectly forward STD :: Forward

Perfect forwarding realizes the function of keeping the value attribute of a parameter during transmission, that is, if it is an lvalue, it will still be an lvalue after transmission, if it is an rvalue, it will still be an rvalue after transmission.

void func(int &arg) {
	std::cout << "func lvalue" << std::endl;
}

void func(int &&arg) {
	std::cout << "func rvalue" << std::endl;
}

template <typename T>
	void wrapper(T &&args) { // Args is an lvalue regardless of the reference type
	func(args);
}

int main(a) {
	int a = 10; 

	wrapper(a); // Pass an lvalue
	wrapper(20); // Pass an rvalue

	return 0; } func lvalue func lvalueCopy the code

The reason for this is that when a func function is called from the Wrapper function, the reference type cannot remain unchanged, that is, the reference property cannot be passed. So C++11 provides the STD ::forward() function for perfect forwarding. That is, in the forwarding process, an lvalue reference retains an lvalue attribute even after being forwarded, and an rvalue reference retains an rvalue attribute even after being forwarded. The modified code looks like this:

void func(int &arg) {
  std::cout << "func lvalue" << std::endl;
}

void func(int &&arg) {
  std::cout << "func rvalue" << std::endl;
}

template <typename T>
void wrapper(T &&args) {
  func(std::forward<T>(args));
}

int main(a) {
  int a = 10; 

  wrapper(a);
  wrapper(20);

  return 0; } The following output is displayed: func lvalue func rvalueCopy the code

2. Copy and omit

Despite the introduction of movement semantics in C++, there is still room for optimization — rather than calling a meaningless movement constructor once, the compiler should just skip the process — hence copy-elision. However, many people confuse mobile semantics with copy-ellipsis:

  • Movement semantics is a concept proposed by language standards. By writing movement constructors and rvalue qualified member functions that comply with the movement semantics, the flow of resource transfer within the object can be logically optimized
  • Copy ellipsis is (C++ 17 Non-standard compiler optimizations that skip the move/copy constructor and allow the compiler to construct the moved object directly on the memory of the moved object
std::unique_ptr<int> foo(a) {
  auto ret = std::make_unique<int> (1);
  / /...
  return std::move(ret);  // -> return ret;
}
Copy the code

There is no need to use STD ::move() to move non-reference return values. C++ treats the return value of a non-reference type that is leaving scope as an rvalue and constructs the returned object with a move (language standard); If the compiler allows you to copy elision, you can also omit the construction of this step and put the RET directly into the memory of the return value (compiler optimization).

3. Use of STD :: Move and STD :: Forward

3.1. Case1 — Do not move rvalue reference arguments

std::unique_ptr<int> bar(std::unique_ptr<int>&& val) {
  / /...
  return val;    // not compile
                 // -> return std::move/forward(val);
}
Copy the code

The problem with the above code is that it does not use STD ::move() on the return value (the compiler prompts STD ::unique_ptr(const STD ::unique_ptr&) = delete). This is because variables (or arguments) referenced by either an lvalue or an rvalue are lvalues after initialization. Therefore, returning an rvalue reference variable requires an explicit move forward or perfect forward using STD :: Move ()/ STD :: Forward () to “restore” the variable to an rvalue (rvalue reference type).

4. References

Rvalue reference resource management tips talk about perfect forwarding in C++