在阅读《Effective Modern C++》的时候条款12在顺带介绍右值成员函数,给了一个类似这样的例子:
class Obj {
int x_;
public:
explicit Obj(int x) : x_(x) {}
int getX() const {
return x_;
}
void setX(int x) {
x_ = x;
}
};
class Test {
Obj obj_;
public:
explicit Test(int x) : obj_(x) {}
Obj getObj()&& {
return std::move(obj_);
}
Obj& getObj() & {
return obj_;
}
};
为了获取类Test
的一个临时对象的Obj
成员,我们定义了一个右值成员函数,返回一个Obj
对象。当时看到这样的代码我脑中有个念头一闪而过?为什么一定要返回一个Obj
对象而不是返回Obj&&
右值引用呢?这样子不是更符合直觉吗?而且还少调用一次移动构造函数(如果外界也用一个右值引用进行接收的话)。
class Obj {
int x_;
public:
explicit Obj(int x) : x_(x) {}
int getX() const {
return x_;
}
void setX(int x) {
x_ = x;
}
};
class Test {
Obj obj_;
public:
explicit Test(int x) : obj_(x) {}
Obj&& getObj()&& { //返回一个右值引用
return std::move(obj_);
}
Obj& getObj() & {
return obj_;
}
};
如果是一个函数返回一个右值引用的话似乎很容易看出来问题:
Obj&& test(int x) {
return Obj(x);
}
这样显然会core dump,因为右值引用其实也不过是一个引用罢了,而这个引用指向的是一个已经返回的栈上对象,这个对象的内存会随着函数栈的销毁而销毁。
int main() {
cout << test(10).getX() << endl;
return 0;
}
运行程序结果:
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
可以看到程序果然已经崩溃。那么如果我们运行返回右值引用的右值成员函数会怎么样呢?
int main() {
cout << Test(10).getObj().getX() << endl; //输出10
return 0;
}
程序正常运行,输出10。似乎我们可以得出这样一个结论:对于右值成员函数来说是可以返回右值引用的?等等,现在下结论似乎为时过早。上面普通函数返回一个右值引用导致程序崩溃是因为右值引用指向的内存空间已经被释放,而现在这个临时对象的内存是在当前函数栈上的,虽然对象已经析构,但是其指向的内存并没有失效,编译器也不会检查到这种在当前函数栈上的非法内存操作。如果我们手动的把这个临时对象的内存释放掉是不是就可以观察到程序的异常呢?
为了观察到这种现象,我们需要给Test
对象在堆上分配内存,并且用一个右值引用保存这个Test
对象的右值成员函数的返回值,然后再释放该Test
对象,我们再次用这个右值引用去访问数据:
int main() {
Test *t {new Test(10)}; //使用统一初始化
cout << t->getObj().getX() << endl; //10
Obj&& obj = std::move(*t).getObj();
delete t;
cout << obj.getX() << endl; //0
return 0;
}
果然,我们观察到了异常,因为我们期待的是通过右值成员函数我们已经获取到了Test
对象的Obj
对象的右值引用,结果Test
对象析构以后右值引用所指向的Obj
失效了。这就和我们前面对普通函数返回一个右值引用的理解是一致的,如果一个函数返回的是一个右值,那么就应该在返回值阶段就用对象去捕获他,如果使用右值引用的话我们无法保证其所指向的空间是否有效(因为右值rvalue包含消亡值xvalue)。
对于lvalue,xvalue,prvalue,gvalue,rvalue的关系,可以看一下这篇博客:什么是 lvalue, rvalue, xvalue。虽然我也没有看完
因此我们就可以得到一个结论:不要让函数返回右值引用,无论是普通函数(危害更大)还是成员函数,而应该返回一个普通对象去捕获右值。
上面的demo中在释放内存前任何一个地方用普通对象去捕获右值引用都不会出现问题,因为会调用移动构造函数。
另一个稍微有意思的地方在于:如果一个函数返回一个右值引用,那么这个返回值就是一个右值引用的右值,那么他是一个什么值呢?联想到引用折叠的话,应该是一个右值引用。我们可以用下面的代码测试一下(使用《Effective Modern C++》里面提到的通过编译器显示推断类型的方法,条款4):
template<typename T>
class TD;
int main() {
TD<decltype(Test(10).getObj())> rrObj;
}
编译器报错:
/home/edward/C++/code/Temp/main.cpp: In function ‘int main()’:
/home/edward/C++/code/Temp/main.cpp:39:37: error: aggregate ‘TD<Obj&&> rrObj’ has incomplete type and cannot be defined
39 | TD<decltype(Test(10).getObj())> rrObj;
| ^~~~~
我们可以看到的确是一个Obj
对象的右值引用。