Concepts about move
rvalue
and forward
etc, in cpp, it seems important and ambiguous for the most of the people, we try to understand thoese concepts in different levels.
if we can take the addresses
Although there are lots of types such as lvalue
rvalue
xvalue
prvalues
etc, refer to this, we start from the general lvalue and rvalue.
Simply speaking, the value that we can take address is the lvalue (it is on the left side of the expression), the value that we can not take it’s address is an rvalue (it is on the right side of the expression). This article provides a good summary, one principle is if we can we take its address, another principle is can the object be move from. This is the detailed documentation.
How do we express that object in the parameter list of the fuction call? For the lvalue
, we can use pointer to the original object or the reference like this TypeName &
. In particular, if X is any type, then X&&
is called an rvalue reference to X. For better distinction, the ordinary reference X&
is now also called an lvalue reference.
Before the rvalue reference
lvalue and rvalue exists before the creation of rvalue reference, here are two typical examples:
std::string name0 = "rvalue"; |
for the second line, if we use the std::string& name = "rvalue"
, there is 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 |
This example shows that the const lvalue reference
can either reference to an lvalue or an rvalue without doing the deep copy. That is the whole point of references.
Before the creation of &&
notation , things can still go as it is. But one human nature is that, we may not always satisfy the current situation, and we want to do sth to optimize the current situation. So we might ask, what is the limitation of the const lvalue reference
. It is obvious that with the limitaion of the const, we can not actually modify the contents of current object. In the previous exmaple, if we try to do:
const std::string& name = "rvalue"; |
we got this error for compiling:
cannot assign to return value because function 'operator[]' returns a const value |
so in this case, we need to copy an object anyway if we want to update the contents in it. This is not flexible in some situations, for example, if the original object is a temprary one, we copy copntent of this opject and then udpate the content in new object, this copy operation is unnecessary. If we just want to keep the inner contents of original objects and replace its old shell/container (the old object), we need an new abstraction that can represent the inner things in the container. Just as the metaphore in this article “when you sell your old property and move to a new house, you do not have to toss all the furniture”. That is the motivation use case of the rvalue reference and the move semantics.
basic move semantic
The std::move
function did not move anything, it just cast the varaible into the rvalue. Simply speaking by code, when there is a vector 1 and we want to get vector 2 from vector 1. Instead of using explicit copy, we can get the inner data of the vector 1, and cast it into an rvalue and assigne it to the vector 2. By this way, the vector 2 is responsible for the inner data. It looks that the inner data is moved from the vector 1 into the vector 2 externally, but actually, we did not copy anything, just change the ownership of the inner value. It might be convenint to consider the vector as an container and we move inner object back and forth logically.
This is the sample code to show what we described:
|
another small thing is the compiler optimization for the return value. We do not always need to consider the extra copy things and use the && or move extensively, we only use it when it is necessary.
for this sample code:
#include <iostream> |
the constructor is called only once, so we could make sure there is the optimization about the construction of the return value. This means that it is ok to return an struct and assing it to the new variable without worrying that this data structure is created twice.
copy constructor vs the move constructor
Another situation that we use the rvalue extensively is the case for the move constructor. This question provides lots of insights. The move constructor is always adopted firstly compared with the copy constructor which uses the const lvalue reference
as the parameter. As mentioned in this article, copy constructor is always the second choice if we define a move constructor which uses rvalue as the parameter.
this is a good example to show relationship between move operation, const lvaue reference and the rvalue reference
#include <stdio.h> |
Let’s dive into the ouput one by one, for the testCopy()
:
=====start testCopy()===== |
we create the res1 by default constructor and then use the copy constructor to create the res2, the inner value for these two instance is res1, and then two dectructors are called.
For the testAssign()
:
=====start testAssign()===== |
For the first two line, two objects are created based on default constructor, then we execute res1=res2
, here, one temp object is created for copy-and-swap based assignment implementation. So in the assignment function, we created a temporary object. Because of the swap operation, the value in the object is changed from the res1 into the res2, when the assignment function finish, the destructor res2
is printed out to show that the temp object is destroyed. At last, when the test function finish, both the res1 and res2 object is destroyed, both of these two objects have the inner value res1
in this case.
For the testRValue1()
:
=====start testRValue1()===== |
There is a slightly difference about the destructor calling. Since the ResourceOwner("res1");
is a temporary object, it is deleted after this line, so this is why there are two destructor log before the destructors for stack vars
, one is from the destroying temporary object which is created in the assignment function, another is from the ResourceOwner("res1")
in this line. At last, the destructor for res2 is called (it has the inner value res1
becasue of the assingment operation).
For the testRValue2()
:
=====start testRValue2()===== |
The printed results are same with the previous one, this aims to show that when we use the std::move(ResourceOwner("res1"))
, but we do not define the move assignment operator explicitly, the original assignment function which has the const lvalue parameter is still be called.
For the testRValue3()
:
=====start testRValue3()===== |
we update the original ResourceOwner
class and created ResourceOwnerWithMove
class. The only differnece is that we add a move assingment operator. If we use the std::move(ResourceOwner("res1"))
, this move assignment operator is called with first priority. Then since we use the swap in the move function, the value in the temporary operator is changed to the res2, which explains the destructor res2
when it is deleted. Finally, the original instance (with res1 in it) is destroyed.
However, we can achieve different version of the move assignment operator based on the flexibility of rvalue reference notation. Since we get rid of the const limitation in this case, so it is flexible to update the inner value. For example, we can update the move assignment operator:
ResourceOwnerWithMove& operator=(ResourceOwnerWithMove&& other) { |
In this way, the original pointer is set as null and we assign it to the pointer at the left side of the object. In this way, we avoid the copy of the inner object and just transfer the ownership of the inner value.
range based for loop
refer to this question
references
http://thbecker.net/articles/rvalue_references/section_03.html
https://juejin.cn/post/6844903497075294216
move ways to implement the move constructor
http://www.vollmann.ch/en/blog/implementing-move-assignment-variations-in-c++.html