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.