The English version is published in Hackernoon and archived on my blog.

This paper is a Chinese remake.

As C++ went further and further in terms of performance and extensibility, it sacrificed ease of use and became harder to learn from one version to the next. This article mainly discusses the new version of C++ related knowledge, rvalue, rvalue reference (&&), and move semantics, hoping to help you solve these difficult points at once.

Rvalue (r-value)

To put it simply, an Rvalue is the value on the right-hand side.

In code:

int var; // too much JavaScript recently:)
var = 8; // OK! l-value (yes, there is a l-value) on the left
8 = var; // ERROR! r-value on the left
(var + 1) = 8; // ERROR! r-value on the leftCopy the code

Simple enough. Let’s look at a more subtle case where the function returns an Rvalue. In code:

#include <string>
#include <stdio.h>

int g_var = 8;
int& returnALvalue() {
   return g_var; //here we return a left value
}

int returnARvalue() {
   return g_var; //here we return a r-value
}

int main() {
   printf("%d".returnALvalue()++); // g_var += 1;
   printf("%d".returnARvalue());
}Copy the code

Results:

8 and 9Copy the code

Note that the function returned lvalues in my example is only for demonstration purposes, so don’t imitate them in real life.

What is the use of an Rvalue

In fact, rvalues have been influencing code logic since before rvalue references (&&) were invented. Take this line of code:

const std::string& name = "rvalue";Copy the code

No problem, but here’s the line:

std::string& name = "rvalue"; // use a left reference for a rvalueCopy the code

Is not compilable:

error: non-const lvalue reference to type 'std::string' (aka 'basic_string<char, char_traits<char>, allocator<char> >') cannot bind to a value of unrelated type 'const char [7]'Copy the code

The compiler forces us to use a constant reference to an Rvalue.

Here’s a more interesting 🌰 :

#include <stdio.h>
#include <string>

void print(const std::string& name) {
    printf("rvalue detected:%s\n", name.c_str());
}

void print(std::string& name) {
    printf("lvalue detected:%s\n", name.c_str());
}

int main() {
    std::string name = "lvalue";
    print(name); //compiler can detect the right function for lvalue
    print("rvalue"); // likewise for rvalue
}
Copy the code

Running result:

lvalue detected:lvalue
rvalue detected:rvalueCopy the code

This indicates that the difference is sufficient for the compiler to decide to overload the function.

So the Rvalue is a constant?

Not really. This is where && comes in.

In code:

#include <stdio.h>
#include <string>

void print(const std::string& name) {
  printf(" const value detected: % s \ n ", the name the c_str ()); } voidprint(std::string& name) {
  printf(" lvalue detected % s \ n ", the name the c_str ()); } voidprint(std::string&& name) {
  printf(" rvalue detected: % s \ n ", the name the c_str ()); } intmain() {STD ::string name = "lvalue"; Const STD ::string cname = "cvalue";print(name);
  print(cname);
  print(" rvalue "); }Copy the code

Running result:

lvalue detected:lvalue
const value detected:cvalue
rvalue detected:rvalueCopy the code

If there is a special rvalue overload function, the rvalue passer selects the special function (the one that takes &&) rather than the more general function that takes constant references. Therefore, && can be more refined with rvalues and constant references.

I’ve summarized the fit between the function argument (the actual variable passed) and the parameter (the variable declared in parentheses). If you’re interested, you can check by changing 🌰 above:

Dividing constant references into constant references and Rvalues is a good idea, but it still doesn’t answer the question of how useful it is.

What problem has been solved?

The problem is that when the parameter is rvalue, the deep copy is unnecessary.

More specifically, && is used to distinguish rvalues so that deep copies can be avoided in functions where the Rvalue 1) is an argument to a constructor or assignment function, and 2) corresponds to a class that contains Pointers to a dynamically allocated resource (memory).

In code, you can be a little more specific:

#include <stdio.h>
#include <string>
#include <algorithm>

using namespace std;

class ResourceOwner {
public:
  ResourceOwner(const char res[]) {
    theResource = new string(res);
  }

  ResourceOwner(const ResourceOwner& other) {
    printf("copy %s\n", other.theResource->c_str());
    theResource = new string(other.theResource->c_str());
  }

  ResourceOwner& operator=(const ResourceOwner& other) {
    ResourceOwner tmp(other);
    swap(theResource, tmp.theResource);
    printf("assign %s\n", other.theResource->c_str());
  }

  ~ResourceOwner() {
    if (theResource) {
      printf("destructor %s\n", theResource->c_str());
      delete theResource;
    }
  }
private:
  string* theResource;
};

void testCopy() {
 // case 1
  printf("=====start testCopy()=====\n");
  ResourceOwner res1("res1");
  ResourceOwner res2 = res1;
  //copy res1
  printf("=====destructors for stack vars, ignore=====\n");
}

void testAssign() {
 // case 2
  printf("=====start testAssign()=====\n");
  ResourceOwner res1("res1");
  ResourceOwner res2("res2");
  res2 = res1;
 //copy res1, assign res1, destrctor res2
  printf("=====destructors for stack vars, ignore=====\n");
}

void testRValue() {
 // case 3
  printf("=====start testRValue()=====\n");
  ResourceOwner res2("res2");
  res2 = ResourceOwner("res1");
 //copy res1, assign res1, destructor res2, destructor res1
  printf("=====destructors for stack vars, ignore=====\n");
}

int main() {
  testCopy();
  testAssign();
  testRValue();
}Copy the code

Running result:

=====start testCopy()=====copy res1=====destructors for stack vars, ignore=====destructor res1destructor res1=====start testAssign()=====copy res1assign res1destructor res2=====destructors for stack vars, ignore=====destructor res1destructor res1=====start testRValue()=====copy res1assign res1destructor res2destructor res1=====destructors for stack vars, ignore=====destructor res1Copy the code

The results in the first two examples, testCopy() and testAssign(), are fine. It makes sense to copy the resources in res1 to res2 because each individual needs its own resource (string).

But not in the third case. The deep copy object res1 is an Rvalue (the return value of ResourceOwner(” res1 “)) and is about to be reclaimed. So you don’t need to have exclusive resources.

Let me repeat the description of the problem again, this time it should be easier to understand:

&& is used to distinguish rvalues so that deep copies can be avoided in functions where the Rvalue 1) is an argument to a constructor or assignment function, and 2) corresponds to a class that contains Pointers to a dynamically allocated resource (memory).

If a deep copy of an Rvalue resource is not appropriate, what is appropriate? The answer is

Move

Moving on to move semantics. The solution is simple. If the parameter is an Rvalue, do not copy it, but simply “move” the resource. Let’s first override the assignment function with an Rvalue reference:

ResourceOwner& operator=(ResourceOwner&& other) {
  theResource = other.theResource;
  other.theResource = NULL;
}Copy the code

This new assignment function is called the move assignment function. The move constructor can be implemented in much the same way, but I won’t go into that here.

For example, if you sell an old house and move to a new one, you don’t have to throw away all the furniture to buy a new one (as we did at 🌰3). You can also “move” furniture to your new home.

Perfect.

What about STD ::move?

Finally, let’s solve STD ::move.

Let’s start with the question:

When 1) we know that an argument is an Rvalue, but 2) the compiler does not know, the move overload function cannot be called.

A common case is to add a layer of ResourceHolder class on top of resource Owner

holder
 |
 |----->owner
         |
         |----->resourceCopy the code

Note that in the code below, I have added the move constructor as well.

In code:

#include <string>
#include <algorithm>


using namespace std;

class ResourceOwner {
public:
  ResourceOwner(const char res[]) {
    theResource = new string(res);
  }


  ResourceOwner(const ResourceOwner& other) {
    printf(" copy % s \ n ", other theResource - > c_str ()); theResource = new string(other.theResource->c_str()); } ++ResourceOwner(ResourceOwner&& other) { ++printf(" move cons % s \ n ", other) theResource - > c_str ()); ++ theResource = other.theResource; ++ other.theResource = NULL; ++} ResourceOwner& operator=(const ResourceOwner& other) { ResourceOwner tmp(other); swap(theResource, tmp.theResource);printf(" the assign % s \ n ", other theResource - > c_str ()); } ++ResourceOwner& operator=(ResourceOwner&& other) { ++printf(" move the assign % s \ n ", other) theResource - > c_str ()); ++ theResource = other.theResource; ++ other.theResource = NULL; + +} ~ResourceOwner() {
    if (theResource) {
      printf(" destructor % s \ n ", theResource - > c_str ()); delete theResource; } } private: string* theResource; }; The class ResourceHolder {... ResourceHolder& operator=(ResourceHolder&& other) {printf(" move the assign % s \ n ", other) theResource - > c_str ()); resOwner = other.resOwner; }... private: ResourceOwner resOwner; }Copy the code

In the ResourceHolder move assignment function, we actually want to call the move assignment function, because the rvalue member is also an Rvalue. but

resOwner = other.resOwner

Is it calling a normal assignment, or is it making a deep copy.

So repeat the question again and see if it makes sense:

When 1) we know that an argument is an Rvalue, but 2) the compiler does not know, the move overload function cannot be called.

Instead, we can use STD ::move to force the variable to an Rvalue and call the correct overloaded function.

ResourceHolder& operator=(ResourceHolder&& other) {
  printf(" move the assign % s \ n ", other) theResource - > c_str ()); resOwner = std::move(other.resOwner); }Copy the code

Can you go a little deeper?

Absolutely!

We all know that in addition to shutting up the compiler, a strong spin actually generates the corresponding machine code. (this is easy to observe without opening O.) This machine code moves the variables around in registers of different sizes to actually perform the strongturn operation.

So does STD ::move do something similar to the strong turn? I don’t know. Let’s try it.

First, let’s change main (I’m trying to be logically consistent)

In code:

int main() {
  ResourceOwner res(“res1”);
  asm(“nop”); // remeber me
  ResourceOwner && rvalue = std::move(res);
  asm(“nop”); // remeber me
}Copy the code

Compile it, and then type the assembly language with the following command

clang++ -g -c -std=c++11 -stdlib=libc++ -Weverything move.cc
gobjdump -d -D move.oCopy the code

😯, the original hidden below the painting style is like this:

0000000000000000 <_main>:
 0: 55 push %rbp
 1: 48 89 e5 mov %rsp,%rbp
 4: 48 83 ec 20 sub $0x20,%rsp
 8: 48 8d 7d f0 lea -0x10(%rbp),%rdi
 c: 48 8d 35 41 03 00 00 lea 0x341(%rip),%rsi # 354
 <GCC_except_table5+0x18>
 13: e8 00 00 00 00 callq
 18 <_main+0x18> 18: 90 nop // remember me
 19: 48 8d 75 f0 lea -0x10(%rbp),%rsi
 1d: 48 89 75 f8 mov %rsi,-0x8(%rbp)
 21: 48 8b 75 f8 mov -0x8(%rbp),%rsi
 25: 48 89 75 e8 mov %rsi,-0x18(%rbp)
 29: 90 nop // remember me
 2a: 48 8d 7d f0 lea -0x10(%rbp),%rdi
 2e: e8 00 00 00 00 callq 33 <_main+0x33>
 33: 31 c0 xor %eax,%eax
 35: 48 83 c4 20 add $0x20,%rsp
 39: 5d pop %rbp
 3a: c3 retq
 3b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)Copy the code

I can’t understand it either. I dyed it with NOP. Looking at the middle part of the two NOPs does generate some machine code, but the machine code seems to do nothing but simply assign the address of one variable to the other. Also, if we turn on O (-O1 is enough), all the machine codes in the middle of the NOP are killed.

clang++ -g -c -O1 -std=c++11 -stdlib=libc++ -Weverything move.cc
gobjdump -d -D move.oCopy the code

Again, if I change the key line to

ResourceOwner & rvalue = res;Copy the code

The machine code is the same except for the relative deviation of the variables.

This means that STD ::move is pure syntactic sugar and has no actual operation.




All right, that’s it for today. If you liked this post, feel free to like and follow. Go to Medium and have sex with my other posts. Thanks for reading.