Modern C++ 学习笔记 -- 左值与右值
左值(lvalues)与右值(rvalues)的概念
左值和右值是Modern C++中引入的新概念。简而言之:
左值位于等号左边,我们可以对左值进行取地址操作。右值位于等号右边,本质上是一个数值,即 literal constant,我们没法对它进行取地址操作。1
int x = 999; // x 是左值, 999是右值
我们,可以把左值大致想象为一个容器,而容器中存放的就是右值,如果没有这个容器,其中的右值将会失效。对于以下的程序,我们在编译的时候将会得到类似的错误:
1
error: lvalue required as left operand of assignment
1
2
int x;
123 = x;
很显然,等号左边需要的是一个左值,而123作为一个 literal constant 类型,是没有办法充当左值的。同样,我们也没法对一个右值进行取地址的操作:
1
2
int *x;
x = &123; //无法对右值取地址
编译器报错:
1
error: lvalue required as unary '&' operand
左值到右值的隐式转换
左值在很多情况下有可能被转换为右值,比如在C++中的 -
运算符,它将两个右值作为参数,并将计算结果作为右值返回。
1
2
3
int x = 10;
int y = 5;
int z = x - y;
在上面的程序片段中,我们明显看到x, y本身是左值,但是却以右值的身份参与了减法运算。这是如何做到的呢?答案是编译器对左值做了隐式的转换,将x和y转换成为了右值。C++中其他的乘法,除法和加法运算也是同样如此。
如果左值能被转换成右值,那么右值本身能被转换成左值吗?答案是 否定 的.
左值引用与右值引用
C++中引入引用的概念,是为了在程序中方便的通过引用修改原变量的值,并且,在调用方法传参的过程中,传递引用可以避免拷贝。在通常情况下,左值引用只能指向左值,而右值引用只能指向右值。听起来比较废话,但是也有特殊的情况。
左值引用
1
2
3
int x = 10;
int& ref_x = x;
ref_x++;
在上面的示例程序中,我们定义了一个左值x
,然后赋值10。随后定义了一个引用,指向x
。因此,ref_x
成为x
的引用,它就叫做左值引用。通过操作ref_x
,我们就可以改变x
的值。
如果我们把上面的程序简化为:
1
int& ref_x = 10;
在编译的时候,我们会得到类似的错误:
1
cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
显然,左值引用只能指向一个左值,而不能指向一个右值。不错从错误信息中,我们方法可以得出另外一种写法:
1
const int& ref_x = 10;
根据编译器的规则,我们被允许通过定义一个const类型的左值来指向右值。不过既然这个左值被定义成了const
,没有办法修改指向的值。
右值引用
C++中的右值引用以&&
表示。通过右值引用,可以修改其指向的右值。
1
2
int&& ref_x = 10; //定义右值引用
ref_x--; //通过右值引用修改其指向的右值
如果我们尝试将右值引用指向一个左值:
1
2
int x = 10;
int&& ref_x = x;
编译器也会抛出类似的错误,告诉我们不能把一个右值引用指向左值。
1
error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
左右值引用的本质
通过一个简单的示例程序,我们就能知道左值引用和有值引用的本质。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void increase(int&& input) {
input++;
}
int main() {
int x = 10;
int& ref_a = &x;
int&& ref_b = std::move(x);
increase(x); // 编译错误,不能传入左值
increase(ref_a); // 编译错误,不能传入 左值引用
increase(ref_b); // 编译错误,右值引用本身是一个左值
increase(std::move(a)); // 编译通过
increase(std::move(ref_a)); // 编译通过
increase(std::move(ref_b)); // 编译通过
increase(7); //编译通过,7是右值
return 0;
}
从上面的代码示例中,我们可以看出右值引用 ref_b
本身也是一个左值,在调用 increase
的时候,需要通过 std::move
转换为右值,编译器才不会报错。
通过以上的例子,我们可以总结出如下的规律:
左右值引用的引入,都是为了避免拷贝。左值引用通常指向左值,通过添加const
关键字约束也能指向右值,不过无法对右值进行修改。右值引用本质上也是一个左值,右值引用通常情况下指向右值,不过通过 std::move
等形式也可以指向左值。右值引用与移动语义
在前面的例子中,我们已经涉及到了 std::move
这样的操作。右值引用配合 std::move
通常能实现移动语义,从而实现避免拷贝,提升程序性能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() {
std::vector<std::string> list;
std::string str_a = "Hello";
std::string str_b = "World";
list.push_back(str_a);
list.push_back(std::move(str_b));
std::cout << "str_a: " << str_a << std::endl;
std::cout << "str_b: " << str_b << std::endl;
std::cout << "list[0]: " << list[0] << std::endl;
std::cout << "list[1]: " << list[1] << std::endl;
return 0;
}
如果运行上面的示例程序,我们会得到这样的程序输出:
1
2
3
4
str_a: Hello
str_b:
list[0]: Hello
list[1]: World
很明显,在str_a
被添加到vector的时候,并没有涉及到移动语义,所以str_a
的值被拷贝到了vector中。而在把str_b
添加到vector的过程中,由于用到了std::move
,所以str_b
的值被移动
到了vector中。之后再输出vector的值的时候,可以看到其中已经包含了str_a
和str_b
的值。但是str_b
本身的值已经被偷走
了。
需要注意的是,std::move
本身的名字比较有迷惑性,其实它在这里的工作只是把str_b
从左值转换成右值,而不会做实际上移动资源的操作。如果我们把添加str_b
的代码替换成:
1
list.push_back(static_cast<std::string&&>(str_b));
会达到一样的效果。而真正的秘密在于 std::vector
提供的两种不同的重载方法:
1
2
void push_back( const T& value );
void push_back( T&& value );
第一个重载方法接受的是左值引用,当传入 str_a
的时候,由于 const
关键字的限制,它的值会被拷贝一份,并放入到vector中,而 str_a
本身的值并不受影响。而第二个重载方法接受的是一个右值引用,push_back方法会把其值放入vector中,并转移 str_b
对字符串值 World
的所有权。这样,当我们再输出它的值的时候,发现已经为空了。
完美转发(std::forward)
完美转发(Perfect Forwarding),转发的意义在于当一个方法把其参数传递给另外一个方法的时候,不光转发参数本身,还包括其属性(左值引用保持左值引用,右值引用保持右值引用)。
std::forward
实际上也是做类型的转换,不同的是 std::move
只把左值转换为右值,std::forward
能转换为左值或右值。
std::forward<T>(arg)
中,如果类型 T
是左值,参数 arg
将被转换为左值,否则 arg
将被转换为右值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void target_func(int& arg) { std::cout << "lvalue reference" << std::endl; }
void target_func(int&& arg) { std::cout << "rvalue reference" << std::endl; }
template <typename T> void forward(T&& arg) {
target_func(std::forward<T>(arg));
}
int main() {
forward(5);
int x = 5;
forward(x);
return 0;
}
在以上的示例程序中,forward
使用一个Universal Reference类型接受一个参数,并且通过 std::forward
讲参数转发给 target_func
。由于此方法有两个重载,分别接受左值引用和右值引用。在我们分别传入右值 5
和左值 x
的时候,我们发现 forward
这个方法都能准确无误的把参数转发给对应的重载方法。因此,程序的输出分别是:
1
2
rvalue reference
lvalue reference
参考文章
Understanding the meaning of lvalues and rvalues in C++Cpp ReferencePerfect Forwarding文章来源:
Author:Timothy
link:https://xiaozhou.net/study-notes-of-modern-cpp-lvalues-and-rvalues-2023-09-06.html