0.Overview
众所周知,C++11 引入了左右值的值类别,使得我们能够根据一个对象的所有权语义、将对象内部的资源“移动”或复制到一个新的对象中。
在转移对象资源的所有权时,为了简化使用 static_cast<A&&>(X)
这样的类型转换语句,标准库提供了一个类型转换函数 std::move
用于将任意一个值类别的对象转换为右值类型。
所以说
std::move
“移动了”对象是不正确的,std::move
不移动任何对象,正如std::forward
没有转发任何资源1,它们都只是一个类型转换函数,并把对应形参的函数引入重载决议中。
那么这里就有一个很经典的考虑:在函数返回值处需不需要使用 return std::move(X)
。
当然,正常情况下是绝对不要使用这种方式返回一个局部作用域的对象的。
为什么?正如上文说明的,std::move
只是一个类型转换函数,它负责批量生产右值引用对象,真正的纯右值类型是无法被我们写出来的,所以如果在常规情况下使用 return std::move(X)
,那么将会返回一个局部作用域的右值引用对象。
右值引用也是引用,所以这种方式会返回一个局部作用域对象的引用,进而导致返回了一个垂悬引用;接下来程序马上就要 crash 了。
进一步的,在 C++17 后,RVO(返回值优化)和 NRVO(具名返回值优化)进入标准,(无论在此之前还是后,)返回一个右值引用对象还会影响函数重载决议(体现在阻止 NRVO 上)。简单来说,参照以下示例,一个使用 return std::move(X)
的函数会比使用 return X
要多一步移动构造。
测试程序见此。
那么问题是,什么情况下才需要使用 return std::move(X)
?
1.被转移的对象的生命周期大于函数作用域时
对,生命周期。尽管 C++ 的类型系统并没有将生命周期引入每个对象的类型属性中,但在编译器进行分析优化时依然会做生命周期分析。
在什么情况下,被转移的对象的生命周期会大于函数作用域?
很典型的就是一个持有了数据成员的类对象。在类方法中,类的数据成员不是隐式的可移动实体;在这类对象中,如果需要在一个 release
方法中放弃资源所有权,那么必须在成员方法中使用 return std::move(X)
才能将资源的移动构造函数引入重载决议,进而正确转移而非拷贝一份资源副本。
在上图的程序中,如果移除了 return
语句中的 std::move
还会导致编译错误(因为 std::unique_ptr
的拷贝构造被删除了)。
测试程序见此。
2.结构化绑定的变量
C++17 引入了一个新的语法:结构化绑定;其实这是一个语法糖。
具体地说,编译器会将这样的语句:
// 对于数组类型
int arr[2] { 0, 1 };
auto [a, b] = arr;
// 对于元组类型
auto [c, d, e] = std::make_tuple<int, bool, double>(42, false, 3.14);
// 对于常规结构体
auto [f, g] = std::make_pair<int, int>(114, 514);
翻译为以下语句:
// 对于数组类型
int arr[2] { 0, 1 };
int __arr[2] = arr; // 复制构造一个隐式对象,这里的名称和构造方式仅作为伪代码演示,下同
int &a = __arr[0], &b = __arr[1];
// 对于元组类型
auto __tuple_obj = std::make_tuple<int, bool, double>(42, false, 3.14);
int &c = get<0>(__tuple_obj); // 要注意这里的 `get` 函数调用不是具名调用;如果在类型的名称域内找不到对应合适的 `get` 函数,就会启用 ADL
bool &d = get<1>(__tuple_obj);
double &e = get<2>(__tuple_obj);
// 对于常规结构体
auto __struct_obj = std::make_pair<int, int>(114, 514);
int &f = get<0>(__struct_obj), &g = get<1>(__struct_obj);
可以看出,翻译后的变量类型全部都是引用类型。尽管我们在使用时,通过 decltype
获取的结构化绑定的变量的类型并没有引用修饰,但这其实是一种隐式的引用关系。
这就是结构化绑定作为语法糖,在某些方面的局限性(摊手.jpg)。此时如果直接 return
一个结构化绑定的变量,实际上是在返回一个隐式引用对象的拷贝副本;所以这里需要使用 return std::move(X)
将移动构造引入重载决议。
进一步的,如果我们查看程序编译后得到的汇编代码,会惊讶地发现:所有具名变量的绑定变成了 get
函数的调用,并且该函数返回的是 tuple 对象中的引用(或者汇编层面应该叫地址)。
这就是结构化绑定作为一个语法糖的证据,它并没有真的去创建多个值类型的变量,而是帮我们使用 get
方法去获取一个隐式对象中的类型引用,我们对结构化绑定变量做的一切工作都是在对这个隐式的对象进行操作。
测试程序见此。
C++ 的编译器真是不辞辛劳地给大伙整麻烦(无奈.jpg)。
详见《Effective Modern C++》Chapter 5,Item 23 ↩︎