编程语言——C++右值和右值引用
C++右值和右值引用是最容易用错的特性之一,相关教程普遍写得不清晰。实际上,只有在所有权转移时才需要使用右值引用,移动语义移动的是所有权,完美转发转发的也是所有权。
总结
先放总结
- 左值引用的语义是绑定、const指针和不转移所有权(rust称为借用),右值引用的语义是绑定、const指针和转移所有权
- 是否使用右值引用,取决于是否要转移变量所有权。右值引用和左值引用在传参时都不会调用构造函数,性能一样高效。
- 如果是字面量, 如果只是读字面量,直接用const& 参数接收即可 (最常见)。如果要转移所有权,例如使用容器时,字面值在当前函数创建随即交给容器,字面量的生命周期后续由容器负责。这时候容器函数应该使用&&,例如emplace()的接口就是右值引用。const&和&&的开销都是一次字面量的构造函数
- 如果是左值,如果需要转移所有权,推荐使用&&和std::move()。如果不要, 用左值引用或指针即可。是不是要转移所有权,在于你想要这个左值变量在当前函数析构,还是在函数外析构
- 新建对象,通过传std::move()左值,可以通过移动构造加快构造速度。如果传字面量,通过可以通过移动构造加快速度。但如果传左值,则需要调用拷贝构造函数。
- 函数返回值不要考虑右值引用,右值引用只在函数参数上使用
右值和右值引用
也许只有C++语言才有左值和右值的概念,程序语言中,左值和右值普遍的概念分别是变量和字面量。
变量可以认为是到内存地址的映射,(变量->内存地址),通过变量我们可以拿到想访问内存的地址,修改变量也就是修改指定地址中的内存。显然变量,或者说左值是可以取地址的。
字面量是程序中写的值,这完全是编译期的概念。编译器编译成运行的指令,将字面量放入内存中。运行期时,只能通过变量来获取值(指针也是一种变量)。因此,变量/左值是编译期和运行期都存在的概念,而字面量/右值只是编译期的概念!
这也导致了左值引用和右值引用的区别,左值引用同时是编译期语义和运行期语义
- 编译期语义,左值引用是一种静态类型,静态类型也是编译期的语义,运行期不存在静态类型的语义。引用类型的语义是绑定,也就是左值引用可以绑定左值,右值引用可以绑定右值。
- 运行期语义,左值引用在运行期的语义基本等于const 指针, 即可以修改引用的对象,但不能修改指针的指向。
右值引用同样1. 编译期语义,右值引用也是静态类型, 可以绑定右值 2. 运行期语言,右值引用记录字面量对象的地址
注意字面量虽然不能被取地址,但它是有地址的,右值引用就记录字面量对象的地址。因此右值引用可以取地址,修改字面量对象,和左值引用一样。
引用绑定和构造函数
引用类型的编译期语义是绑定,类型的构造函数,即拷贝构造、移动构造、拷贝赋值和移动赋值就是依赖引用类型的绑定来接收左值或右值。
下列代码可以得到
- 拷贝构造和拷贝赋值传入左值引用,移动构造和移动赋值传入右值引用。这代表编译期拷贝函数的参数需要绑定左值,移动函数的参数需要绑定右值。如果传入左值,则不可调用移动函数,反之不可调用拷贝函数。
- 在使用变量创建IntVector时,由于拷贝函数绑定左值,因此会调用拷贝函数; 使用字面量(如IntVector{})创建IntVector,会调用移动函数。
- 移动构造和移动赋值的语义是所有权转移,例如使用字面量创建IntVector{},如auto x = IntVector(IntVector{});, 代表字面量对象的所有权交给了新建的对象x。
移动构造和移动赋值的所有权语言更典型的体现是在容器中,例如vec.emplace_back(1), 表示字面量的所有权交给了vec容器。清理容器时会清理这个字面量,字面量的生命周期由容器控制,不再由函数控制。
std::move可以把左值转为右值,这也是编译期的概念。例如vec.emplace_back(std::move(a)), 表示a的所有权交给了vec容器。清理vec时会清理a,a的生命周期由vec控制,不再由函数控制。 变量通过转换成右值,借助移动构造函数将所有权转移,当前函数后续就不能直接操作这个变量,当前函数不具有变量的所有权。
简单讲一下所有权,所有权代表生命周期控制,函数/对象都可以持有所有权。持有所有权代表函数/类负责对象的生命周期,例如类持有成员变量的所有权吗,当对象的生命周期脱离控制,也就是失去了该对象的所有权。
1 |
|
这部分总结就是
- 左值引用和右值引用的编译期语义分别是绑定左值和右值
- 拷贝函数参数是左值引用,它绑定一个左值,根据这个左值拷贝得到新对象;
- 移动函数参数是右值引用,它绑定一个右值,根据这个右值移动得到新对象。移动构造函数实现时可以移动右值的成员到新对象,可以减少拷贝,这本身是所有权转移,于是右值引用又增加了移动所有权的语义。
前面讲到左值引用和右值引用都有绑定和指针的语义,这里又增加了右值引用的移动所有权的语义,相反左值引用的语义则是不移动所有权
- 需要注意C++的灵活性,你通过左值引用传左值到新函数,同时在新函数将该左值析构。这代码上没错,但违反了左值引用不移动所有权的语义。正确的使用是传通过右值引用传右值到新函数,新函数可以将该右值移动或析构。在rust中,前一种操作会直接编译报错。
- 总之编码时只有右值引用才转移所有权(例如将unique_ptr传给新函数,推荐使用右值引用,因为发生所有权转移)。传指针/引用都不应该转移所有权,这种编码风格才是推荐的。通过函数参数就知道对象被谁管理。
为什么不拷贝/移动函数不使用值传递,因为值传递过程中就需要拷贝,拷贝有需要拷贝函数。。。拷贝函数/移动函数通过左值引用和右值引用,在传参时都不会有拷贝。
移动函数和拷贝函数的调用时机
观察下面程序,可以得到
MyClass bb = a
调用构造函数,MyClass bb; bb = std::move(a)
调用赋值函数。- 执行
c = MyClass{}
,虽然是调用移动函数, 但会MyClass{}字面量的构造需要调用一次构造函数。MyClass bb = a
只需要一次拷贝构造函数,不用构造函数 c = std::move(a);
是真正的只要调用一次移动赋值函数
1 |
|
字面量使用const&和&&的区别,
观察下面程序,可以看到
- const&和&&作为引用,传参时都不会调用构造函数
- 左值优先匹配const&,右值优先匹配&&
- std::move()后的左值优先匹配&&
- const& 函数对传的变量只能只读, 而&&可以修改参数变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39void func(const MyClass& object) {
printf ("const& 接收\n");
}
void func(MyClass&& object) {
printf ("&&接收\n");
}
void func1(const MyClass& object) {
printf ("强制const& 接收\n");
}
int main() {
MyClass object1;
printf ("func(object1);\n");
func(object1);
printf ("func(std::move(object1));\n");
func(std::move(object1));
printf ("func(MyClass());\n");
func(MyClass());
printf ("func1(MyClass());\n");
func1(MyClass());
}
// 输出
构造函数
func(object1);
const& 接收
func(std::move(object1));
&&接收
func(MyClass());
构造函数
&&接收
析构函数
func1(MyClass());
构造函数
强制const& 接收
析构函数
析构函数
右值引用可以设置值,取地址
1 | void func(MyClass&& object) { |
左值引用的取地址是绑定变量的地址
函数返回值
前面讲了函数参数,我们得到当传一个左值或右值且所有权转移,使用右值引用绑定;否则使用左值引用或指针绑定。
引用传参,无论是左值引用还是右值引用,都不会触发构造语义。如果我们返回一个对象呢,例如
1 | MyClass create() { |
事实上返回值一般会执行返回值优化,即返回的对象在调用处直接构造,无须构造临时对象。因此我们没有任何必要调用return std::move(xx)
。
实际编码中,我们推荐
- 使用引用或指针函数参数只读或修改对象
- 用专门的工厂函数创建对象,并返回对象的指针,或者返回shared_from_this()
- 函数返回错误码,尽量避免返回对象
因此右值引用一般只用于函数参数,用来绑定右值并执行移动语义和所有权转移,无须在返回值中考虑右值引用
什么时候用右值引用
由上我们总结, 右值引用和左值引用在传参时都不会调用构造函数,性能一样高效。是否使用右值引用,取决于是否要转移变量所有权
1.如果是字面量, 如果只是读字面量,直接用const& 参数接收即可 。如果要转移所有权,例如使用容器时,字面值在当前函数创建随即交给容器,字面量的生命周期后续由容器负责。这时候容器函数应该使用&&,例如emplace()的接口就是右值引用。使用左值引用接收右值,需要1构造+1拷贝函数,而右值引用需要1构造+1移动函数
2. 如果是左值,如果需要转移所有权,推荐使用&&和std::move()。如果不要, 用左值引用或指针即可。是不是要转移所有权,在于你想要这个左值变量在当前函数析构,还是在函数外析构
3. 移动构造函数使用右值引用作为参数,可以加快构造速度。但移动构造函数只有在新建对象时才使用。
4. 函数返回值不要考虑右值引用,右值引用只在函数参数上使用
完美转发
std::move(x) 将左值转为右值,从而利用右值引用函数参数(右值也是转成右值)
std::forward<T>
, 若T是左值引用,返回左值。若 T 是右值引用,返回右值。完美转发主要处理《右值引用也是一种左值》这个问题,使用forward,可以把右值引用再次转成右值,从而继续使用下一个函数的右值引用函数参数。
所有权语义上分析,完美转发就是在A函数将对象所有权转移到B函数,B函数再将对象所有权转给C函数。移动语义移动的是所有权,完美转发转发的也是对象所有权。
如果参数是通用引用,对于需要转移所有权的右值引用,B函数会将参数传递给C函数,对于无须转移所有权的左值引用,B函数不会转移所有权给C函数
1 |
|
转型、左右值、左右值函数引用,函数参数优先匹配等逻辑,都是编译期的行为。
本文标题:编程语言——C++右值和右值引用
文章作者:Infinity
发布时间:2025-04-22
最后更新:2025-06-02
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!