前言
写了这么十来篇博客,我发现我的写作风格都极其相似。好像还停留在高考那种应试要求的“八股文”一样。前言,提出问题,解决问题,发散思维,总结 可能这种方式更加符合的我思考方式吧,如果有什么更好的推荐方式,可以评论区留言。谢谢大家!!在《effective C++》的条款20中,推荐使用const-by-reference-to-const替代pass-by-value,很多人如果没有看到条款就会在函数返回的时候,返回对象的引用。因为这样可以减少拷贝复制,以及析构带来的效率降低风险。在条款21中,提出如果我们必须返回对象时,千万不要返回引用。这是为什么呢? 如果返回这个对象是在函数内部定义的,那么这个对象的作用域位于函数内部,但是如果返回了这个对象的引用,那么就将这个函数的内存泄漏出去了。
既然不能返回对象的引用,那我们就只能返回对象的值了。这一点C++标准化委员会心里也清楚,所以就对这部分进行了相应的优化。至于如何优化我们就来看下面的内容吧。
旧式解决办法
在C++11之前为了解决这问题,我们通常是由调用者分配好内存空间,然后将这部分空间以引用的形式调用接口。比如下面这种方式:
MyObject mobj;
auto ec = initialize(&mobj);
MyObject initialize(MyObject mobj) {
return mobj;
}
这样是与C兼容的,但是这样就很麻烦,需要调用者做额外的工作。当然是最简单的直接返回对象才最符合调用者的期待啊!
auto ec = initialize();
MyObject initialize() {
MyObject mobj;
return mobj;
}
返回对象
通常我们可以返回的对象,是满足可移动构造/赋值的,一般也是满足可拷贝构造/赋值的,如果这个对象同时又满足可默认构造,那么我们就称这个对象是半正则对象。
根据我们前一个文章所讲,六个特殊函数之间存在相互制约关系。所以我们如果要满足这个半正则对象的要求,我们必须要在这个对象一般构造函数的基础上加上默认构造函数,拷贝构造函数,移动构造函数,拷贝赋值运算符,移动赋值运算符。
在左值右值那一部分里面我说过,函数的返回值是一个纯右值元素。所以在调用临时变量的构造函数的时候优先调用右值重载的移动构造函数。
#include<iostream>
using namespace std;
class A {
public:
A() { cout << "create A()" << endl; }
~A() { cout << "delete A()" << endl; }
A(const A&) { cout << "copy A()" << endl; }
A(A&&) { cout << "move A()" << endl; }
};
template<typename T>
A return_A(T&& a)
{
return forward<T>(a);
}
A makeA() {
return A();
}
int main() {
A a1;
auto a2 = return_A(move(a1));
/*
create A()
create A()
delete A()
delete A()
*/
auto a2 = makeA();
/*
create A()
move A()
delete A()
delete A()
*/
}
在MSVC当中这个优化不是很强大,但是在GCC或者其他的编译器中这的结果是不一样的。
总结
到了这个地方,其实我们需要掌握的内容不是为什么这不同的编译器带来的效果不一致。而是需要知道什么时候需要返回对象,在F.20中提出了几个例外的情况
- 返回对象可能是存在多态特征的,比如通过返回父类对象的值来返回子类对象,这会导致对象切割;
- 移动对象成本很高的情况,这种情况下我们可以考虑把这个对象分配到堆上,然后返回一个指针对象即可;
除了上述这两种情况外,其它的情况大多可以直接返回对象即可。《effective modern C++》中的条款41,提到针对可复制的形参,在移动成本低且一定会被复制的前提下,考虑将其按值传递。