Article directory
write in front
This article mainly sorts out the following issues for you:
- What is an rvalue
- The meaning and usage scenarios of rvalue references
-
std::move
The nature of functions - How to write a move constructor
- Universal citation and perfect forwarding
References:
-
"Understand C++ rvalue references and std::move in one article"
-
"Advanced Knowledge of C++: In-depth Analysis of the Move Constructor and Its Principles"
Due to the author’s limited knowledge and limited knowledge, everyone is welcome to correct me if I have a lack of understanding.
1. What is an rvalue and what is an lvalue?
Each C++ expression (including operators and their operands, literal values, variable names, etc.) has two independent attributes: type and value category :
Everyone is familiar with type (type), which refers to the data type of the expression. , which defines the value range and executable operations of the expression . For example, an integer expression may be of type int, and a floating-point expression may be of type float.
Value category can be understood as the identity and portability of expressions . According to the C++ standard, there are three main value categories: rvalue, lvalue, and xvalue. The relationship between the three is as follows:
- The identity determines whether it has expression addressability, that is, whether we can get its address in memory
- If mobility appears in assignment, initialization, etc. statements, will the statement exhibit move semantics?
The above is somewhat abstract. Let’s analyze it with specific examples and differentiate based on the characteristics:
[Rvalues and rvalue references]:
// 以下都是常见的右值
10;
x + y;
fmin(x, y);
// 右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
- An rvalue is also an expression that represents data, such as: literal constant, expression return value, function return value (this cannot be an lvalue reference return), etc.
- Rvalues cannot be placed to the left of the assignment symbol. If you don’t believe me, try it.
10 = x + y
- A reference to an rvalue is an rvalue reference,
&&
represented by - You cannot get an address from an rvalue. If you don’t believe me, try it.
int* p = &10;
[Lvalue and lvalue reference]:
// 以下a、b、pa都是左值
int a = 10;
int b = a;
int* pa = &a;
// 左值引用
int& rla = a;
- lvalues can appear to the left and right of the assignment symbol
- You can take its address and assign a value to it.
Let’s consider some difficult questions:
1.1 Can rvalue references refer to lvalues?
Can. std::move()
Functions can coerce lvalues into rvalues. That's right, it's forced type conversion . We will talk about it in detail later in conjunction with the source code:
int a = 10;
int&& rr = std::move(a);
1.2 Are lvalue references and rvalue references themselves lvalues or rvalues?
The declared left and right value references are all lvalues. Because according to the C++ language specification, whether they are lvalue references or rvalue references, they are considered named objects, have addresses and can be addressed . Therefore, when using references, they are treated as lvalues. Verify with the following code:
void check(int&& rr) {
cout << "Yes" << endl;
}
int main() {
int a = 5; // a是个左值
int& ref_a_left = a; // ref_a_left是个左值引用,本身是左值
int&& ref_a_right = std::move(a); // ref_a_right是个右值引用,本身是左值
check(a); // 编译不过,无法将左值绑定到右值
check(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
check(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
check(std::move(a)); // 编译通过
check(std::move(ref_a_right)); // 编译通过
check(std::move(ref_a_left)); // 编译通过
}
The rvalue reference itself is an lvalue, so it is not difficult to understand why the rvalue cannot be modified, but the rvalue reference can be modified: we are not modifying the rvalue, but modifying the object referenced by the rvalue reference; rvalue A reference is an lvalue, which has its own identifier and address.
int&& rr = 10;
rr = 20;
1.3 Special const lvalue reference
const lvalue references are special, they can accept both lvalues and rvalues. Like rvalue references, const lvalue references can extend the rvalue 生命周期
to avoid creating dangling references . Here are the instructions from cppreference:
When we bind an rvalue to a const lvalue reference, the compiler automatically creates a temporary object and binds the rvalue to the temporary object. The life cycle of this temporary object will be the same as the life cycle of the const lvalue reference , thus ensuring that this rvalue can be used safely within the scope of the const lvalue reference.
This is one of the reasons why you often see const& used as function parameters. Without const, this code will not compile:v.push_back(5)
void push_back (const value_type& val);
2. The meaning of rvalue reference and move construction
Lvalue references can improve efficiency both as parameters and return values. But the shortcoming of lvalue references is that if the referenced object is scoped, then lvalue references cannot be used. For example, in the following example (without considering compiler optimization), when the hello() function returns, it will first "hello world"
copy to a temporary variable.
This temporary variable essentially belongs to 将亡值
, has identity and mobility at the same time . Before there is a move construct, s can only make another copy of the contents of the temporary variable, and watch helplessly as the temporary variable is destroyed when its life cycle is up - a waste!
string hello() {
return "hello world";
}
string s = hello();
But for an object that is about to be destroyed, why don't we be smarter and directly occupy the resources? Transferring ownership of the other party's resources is the core idea of mobile construction. How to transfer? It can be intuitively understood as an exchange of pointers:
namespace my{
string(string&& s)
:_str(nullptr) ,_size(0), _capacity(0){
swap(str_, s.str_) // 所有权转移
}
};
Note that after move construction, the pointer in the original object must be set to null, otherwise a space will be deleted twice. And for nullptr
, delete multiple times has no effect.
Since move construction is so popular, let us further think about which resources can be moved and constructed? The designers of C++ noted that in most cases, objects contained in rvalues can be moved safely .
So the question is, how do we explicitly tell the compiler that we want to receive an rvalue? Before C++11, only const-modified lvalue references could receive rvalues . This was a big problem. You were modified by const, so how could I "steal" your resources? Following this logical thinking, the emergence of rvalue references is also inevitable, and it spreads fertile soil for the emergence of mobile structures.
Next we will manually implement the move constructor
3. Use of move constructor
namespace my {
class string {
public:
string() : len_(0), cap_(0), data_(nullptr) {
}
string(const char* s) {
// 略
}
string(const my::string& s) {
cout << "拷贝构造函数" << endl;
// 略
}
my::string& operator=(const my::string& s) noexcept {
cout << "赋值运算符重载" << endl;
// 略
}
string(my::string&& s) {
cout << "移动构造函数" << endl;
len_ = s.len_;
cap_ = s.cap_;
swap(data_, s.data_);
}
my::string& operator=(my::string&& s) noexcept{
cout << "移动赋值运算符重载" << endl;
len_ = s.len_;
cap_ = s.cap_;
if (data_) {
delete data_;
data_ = nullptr;
}
swap(data_, s.data_);
return *this;
}
private:
char* data_;
int len_;
int cap_;
};
};
STL libraries basically support move construction and move assignment, such as string, etc.
string (string&& str) noexcept;
string& operator= (string&& str) noexcept;
The swap function is also
to share with you a mistake I easily made when I was new: We still use the my::string class implemented above for testing. Do you think there is s = t.s
any move assignment called in?
class test {
public:
test() {
}
// ……
test(test&& t) {
s = t.s;
}
private:
my::string s;
};
The answer is not. Although t is an rvalue, tm is indeed an lvalue:
4. Implementation principle of move
When you first start learning std::move
, it is always easy for everyone to misunderstand the move function, thinking that the move function completes the movement of resources on the memory. However, in fact, the work completed by move is just forced type conversion . Let’s take a look at the corresponding source code:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_case<typename remove_reference<T>::type&&>(t);
}
Although we don't know the details, we can roughly see that move completes the work of forced type conversion. For a more thorough understanding, I will explain the details for you.
-
In the template,
&&
it does not represent an rvalue reference, but a universal reference . It can receive both lvalues and rvalues. -
What does this return value
typename remove_reference<T>::type&&
mean? type is a type member defined in remove_reference, so when accessing it, use :: to access it the same way as accessing static members. The class is a template class, so the typename keyword must be added in front of it. -
remove_reference
As can be seen from its name, it removes references through templates
-
We assume that T is int&, that is, an lvalue is passed in, then the above code can finally be simplified into the following form:
int && move(int& && t){ return static_case<int&&>(t); }
-
When encountering
int& &&
, reference folding will occur. The folding rules are as shown in the figure below:
-
So in the end move actually does this:
int && move(int& t){ return static_case<int&&>(t); }
5. Perfect forwarding
We mentioned earlier that in a template, &&
you can receive both lvalues and rvalues, but when we pass val to another function inside a function, val will degenerate. At this time, val is always passed as an lvalue .
void Fun(int &x){
cout << "左值引用" << endl; }
void Fun(const int &x){
cout << "const 左值引用" << endl; }
void Fun(int &&x){
cout << "右值引用" << endl; }
void Fun(const int &&x){
cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a = 10;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
}
-
In order to maintain the original left and right value attributes of the parameters, we need to use
std::forward<模板参数>()
functions to achieve perfect forwarding:
-
Notice! In order to maintain the original left and right value attributes of the parameters, all downward forwarding needs to achieve perfect forwarding: