最近,我一直在阅读this post和that post,建议停止返回const对象。
Stephan T. Lavavej在《 Going Native 2013》的his talk中也给出了此建议。
我编写了一个非常简单的测试,以帮助我了解在所有这些情况下调用哪个构造函数/运算符:
这是测试:
#include <iostream>
void println(const std::string&s){
try{std::cout<<s<<std::endl;}
catch(...){}}
class A{
public:
int m;
A():m(0){println(" Default Constructor");}
A(const A&a):m(a.m){println(" Copy Constructor");}
A(A&&a):m(a.m){println(" Move Constructor");}
const A&operator=(const A&a){m=a.m;println(" Copy Operator");return*this;}
const A&operator=(A&&a){m=a.m;println(" Move Operator");return*this;}
~A(){println(" Destructor");}
};
A nrvo(){
A nrvo;
nrvo.m=17;
return nrvo;}
const A cnrvo(){
A nrvo;
nrvo.m=17;
return nrvo;}
A rvo(){
return A();}
const A crvo(){
return A();}
A sum(const A&l,const A&r){
if(l.m==0){return r;}
if(r.m==0){return l;}
A sum;
sum.m=l.m+r.m;
return sum;}
const A csum(const A&l,const A&r){
if(l.m==0){return r;}
if(r.m==0){return l;}
A sum;
sum.m=l.m+r.m;
return sum;}
int main(){
println("build a");A a;a.m=12;
println("build b");A b;b.m=5;
println("Constructor nrvo");A anrvo=nrvo();
println("Constructor cnrvo");A acnrvo=cnrvo();
println("Constructor rvo");A arvo=rvo();
println("Constructor crvo");A acrvo=crvo();
println("Constructor sum");A asum=sum(a,b);
println("Constructor csum");A acsum=csum(a,b);
println("Affectation nrvo");a=nrvo();
println("Affectation cnrvo");a=cnrvo();
println("Affectation rvo");a=rvo();
println("Affectation crvo");a=crvo();
println("Affectation sum");a=sum(a,b);
println("Affectation csum");a=csum(a,b);
println("Done");
return 0;
}
这是 Release模式下的输出(使用NRVO和RVO):
build a
Default Constructor
build b
Default Constructor
Constructor nrvo
Default Constructor
Constructor cnrvo
Default Constructor
Constructor rvo
Default Constructor
Constructor crvo
Default Constructor
Constructor sum
Default Constructor
Move Constructor
Destructor
Constructor csum
Default Constructor
Move Constructor
Destructor
Affectation nrvo
Default Constructor
Move Operator
Destructor
Affectation cnrvo
Default Constructor
Copy Operator
Destructor
Affectation rvo
Default Constructor
Move Operator
Destructor
Affectation crvo
Default Constructor
Copy Operator
Destructor
Affectation sum
Copy Constructor
Move Operator
Destructor
Affectation csum
Default Constructor
Move Constructor
Destructor
Copy Operator
Destructor
Done
Destructor
Destructor
Destructor
Destructor
Destructor
Destructor
Destructor
Destructor
我并不奇怪的是:
为什么在“Constructor csum”测试中使用move构造函数?
返回对象是const,所以我真的觉得它应该调用复制构造函数。
我在这里想念什么?
它不应该是编译器的错误,Visual Studio和clang都可以提供相同的输出。
最佳答案
在这种特殊情况下,允许编译器执行[N] RVO,但没有这样做。第二件事是移动构造返回的对象。
没关系。但是我想这还不是很明显,所以让我们逐步了解一下返回值的概念含义,以及[N] RVO是什么。为此,最简单的方法是忽略返回的对象:
T f() {
T obj;
return obj; // [1] Alternatively: return T();
}
void g() {
f(); // ignore the value
}
在标记为[1]的行中,存在从本地/临时对象到返回值的拷贝。即使该值被完全忽略。这就是您在上面的代码中所执行的操作。
如果您不忽略返回的值,则如下所示:
T t = f();
从返回值到
t
局部变量,在概念上存在第二个拷贝。在您所有的案例中,第二份拷贝都被删除了。对于第一个拷贝,无论返回的对象是否是
const
都无关紧要,编译器根据[conceptual copy/move]构造函数的参数来确定要执行的操作,而不是根据所构造的对象是否是const
来确定要执行的操作。这与以下内容相同:// a is convertible to T somehow
const T ct(a);
T t(a);
不管目标对象是否为const都没有关系,编译器需要根据参数而不是目标来找到最佳的构造函数。
现在,如果将其带回到您的练习中,请确保未调用copy构造函数,您需要修改
return
语句的参数:A force_copy(const A&l,const A&r){ // A need not be `const`
if(l.m==0){return r;}
if(r.m==0){return l;}
const A sum;
return sum;
}
这应该触发拷贝的构造,但是再一次,它很简单,只要编译器认为合适就可以完全删除拷贝。