左值(lvalues)与右值(rvalues)的概念
左值和右值是Modern C++中引入的新概念。简而言之:
- 左值位于等号左边,我们可以对左值进行取地址操作。
- 右值位于等号右边,本质上是一个数值,即 literal constant,我们没法对它进行取地址操作。
1 | int x = 999; // x 是左值, 999是右值 |
我们,可以把左值大致想象为一个容器,而容器中存放的就是右值,如果没有这个容器,其中的右值将会失效。对于以下的程序,我们在编译的时候将会得到类似的错误:
1 | error: lvalue required as left operand of assignment |
1 | int x; |
很显然,等号左边需要的是一个左值,而123作为一个 literal constant 类型,是没有办法充当左值的。同样,我们也没法对一个右值进行取地址的操作:
1 | int *x; |
编译器报错:
1 | error: lvalue required as unary '&' operand |
左值到右值的隐式转换
左值在很多情况下有可能被转换为右值,比如在C++中的 -
运算符,它将两个右值作为参数,并将计算结果作为右值返回。
1 | int x = 10; |
在上面的程序片段中,我们明显看到x, y本身是左值,但是却以右值的身份参与了减法运算。这是如何做到的呢?答案是编译器对左值做了隐式的转换,将x和y转换成为了右值。C++中其他的乘法,除法和加法运算也是同样如此。
如果左值能被转换成右值,那么右值本身能被转换成左值吗?答案是 否定 的.
左值引用与右值引用
C++中引入引用的概念,是为了在程序中方便的通过引用修改原变量的值,并且,在调用方法传参的过程中,传递引用可以避免拷贝。在通常情况下,左值引用只能指向左值,而右值引用只能指向右值。听起来比较废话,但是也有特殊的情况。
左值引用
1 | int x = 10; |
在上面的示例程序中,我们定义了一个左值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 | int&& ref_x = 10; //定义右值引用 |
如果我们尝试将右值引用指向一个左值:
1 | int x = 10; |
编译器也会抛出类似的错误,告诉我们不能把一个右值引用指向左值。
1 | error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' |
左右值引用的本质
通过一个简单的示例程序,我们就能知道左值引用和有值引用的本质。
1 | void increase(int&& input) { |
从上面的代码示例中,我们可以看出右值引用 ref_b
本身也是一个左值,在调用 increase
的时候,需要通过 std::move
转换为右值,编译器才不会报错。
通过以上的例子,我们可以总结出如下的规律:
- 左右值引用的引入,都是为了避免拷贝。
- 左值引用通常指向左值,通过添加
const
关键字约束也能指向右值,不过无法对右值进行修改。 - 右值引用本质上也是一个左值,右值引用通常情况下指向右值,不过通过
std::move
等形式也可以指向左值。
右值引用与移动语义
在前面的例子中,我们已经涉及到了 std::move
这样的操作。右值引用配合 std::move
通常能实现移动语义,从而实现避免拷贝,提升程序性能。
1 |
|
如果运行上面的示例程序,我们会得到这样的程序输出:
1 | str_a: Hello |
很明显,在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 | void push_back( const 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 |
|
在以上的示例程序中,forward
使用一个Universal Reference类型接受一个参数,并且通过 std::forward
讲参数转发给 target_func
。由于此方法有两个重载,分别接受左值引用和右值引用。在我们分别传入右值 5
和左值 x
的时候,我们发现 forward
这个方法都能准确无误的把参数转发给对应的重载方法。因此,程序的输出分别是:
1 | rvalue reference |