如何移动const返回的对象?

时间:2013-12-12 13:35:20

标签: c++ c++11 const rvalue-reference rvo

最近,我一直在阅读this postthat post建议停止返回const对象。 Stephan T. Lavavej也在2013年Going Native的his talk中提出了这个建议。

我写了一个非常简单的测试来帮助我理解在所有这些情况下调用哪个构造函数/运算符:

  • 返回const或非const对象
  • 如果返回值优化(RVO)开始了怎么办?
  • 如果命名返回值优化(NRVO)启动怎么办?

以下是测试:

#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;
}

以下是发布模式下的输出(使用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”测试中使用移动构造函数?

返回对象是const,所以我觉得它应该调用复制构造函数。

我在这里缺少什么?

它不应该是编译器的错误,Visual Studio和clang都会提供相同的输出。

4 个答案:

答案 0 :(得分:4)

  

我不明白的是:为什么在“Constructor csum”测试中使用移动构造函数?

在这种特殊情况下,允许编译器执行[N] RVO,但它没有这样做。第二个最好的事情是移动构造返回的对象。

  

返回对象是const,所以我觉得它应该调用复制构造函数。

这根本不重要。但我想这并不是很明显,所以让我们来看看它在概念上意味着返回一个值,以及[N] RVO是什么。为此,最简单的方法是忽略返回的对象:

T f() {
   T obj;
   return obj;   // [1] Alternatively: return T();
}
void g() {
   f();          // ignore the value
}

在标记为[1]的行中,有一个从本地/临时对象到返回值的副本。 即使该值完全被忽略。这就是你在上面的代码中练习的内容。

如果您不忽略返回的值,请执行以下操作:

T t = f();

概念上,从返回值到t局部变量的第二个副本。在你的所有案件中,第二份副本都被删除了。

对于第一个副本,返回的对象是否为const并不重要,编译器根据[concept copy / move]构造函数的参数确定要执行的操作,而不是对象是否为构造将是const或不。这与:

相同
// a is convertible to T somehow
const T ct(a);
T t(a);

目标对象是否为const无关紧要,编译器需要根据参数找到最佳构造函数,而不是目标。

现在,如果我们重新开始练习,为了确保不调用复制构造函数,您需要修改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;
}

那应该触发复制构造,但是再次很简单,如果发现它适合,编译器可能会完全忽略该副本。

答案 1 :(得分:1)

根据我的观察,移动构造函数优先于复制构造函数。正如Yakk所说,由于多个返​​回路径,你无法忽略移动构造函数。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#Copy%20vs%20Move

  

rvalues更喜欢右值参考。左值优先于左值   引用。简历资格转换被认为是次要的   相对于r / l值转换。 rvalues仍然可以绑定到const   左值参考(const A&amp;),但仅限于没有更多   过载集中有吸引力的右值参考。左值可以绑定   一个右值参考,但如果它存在,则更喜欢左值参考   在过载集中。更符合cv标准的对象不能的规则   绑定到一个较小的cv限定的引用站...对于左值和   右值参考。

     

此时可以进行进一步的语言改进。什么时候   从a返回具有自动存储的非cv限定对象   函数,应该有对rvalue的隐式强制转换:

string
operator+(const string& x, const string& y)
{
    string result;
    result.reserve(x.size() + y.size());
    result = x;
    result += y;
    return result;  // as if return static_cast<string&&>(result);
}
     

这种隐式演员产生的逻辑导致自动   “移动语义”从最好到最差的层次结构:

If you can elide the move/copy, do so (by present language rules)
Else if there is a move constructor, use it
Else if there is a copy constructor, use it
Else the program is ill formed

那么如果删除参数中的const &怎么办?它仍然会调用move构造函数,但会调用参数的复制构造函数。如果你返回一个const对象怎么办?它将调用局部变量的复制构造函数。如果您返回const &怎么办?它还将调用复制构造函数。

答案 2 :(得分:1)

答案是您的A sum局部变量被移动到函数返回的const A(这是移动构造函数输出),然后将返回值复制到{{1}编译器会删除它(因此没有Copy Constructor输出)。

答案 3 :(得分:1)

我反汇编了编译的二进制文件(VC12发布版本,O2),我的结论是:

move操作是在返回到堆栈分配的csum(a,b)临时对象之前在const A内移动结果,以用作稍后A& operator=(const A&)的参数。

move操作不能move cv限定变量,但在从csum返回之前,sum变量仍然是非常量变量,因此可以{ {1}};并且需要moved以便以后使用。

moved修饰符仅在返回后禁止编译器const,但不禁止move内的move。如果您从csum中移除const,则结果为:

csum

顺便说一句,你的测试程序有一个错误,会导致Default Constructor Move Constructor Destructor Move Operator Destructor 不正确,A的默认ctor应该是:

a = sum(a, b);

或者您会发现您的给定输出难以解释A() : m(3) { println(" Default Constructor"); }


下面我将尝试分析调试构建ASM。结果是一样的。 (分析版本构建就像自杀&gt; _&lt;)

主:

a = sum(a, b);

CSUM:

  a = csum(a, b);
00F66C95  lea         eax,[b]  
00F66C98  push        eax                           ;; param b
00F66C99  lea         ecx,[a]  
00F66C9C  push        ecx                           ;; param a
00F66C9D  lea         edx,[ebp-18Ch]  
00F66CA3  push        edx                           ;; alloc stack space for return value
00F66CA4  call        csum (0F610DCh)  
00F66CA9  add         esp,0Ch  
00F66CAC  mov         dword ptr [ebp-194h],eax  
00F66CB2  mov         eax,dword ptr [ebp-194h]  
00F66CB8  mov         dword ptr [ebp-198h],eax  
00F66CBE  mov         byte ptr [ebp-4],5  
00F66CC2  mov         ecx,dword ptr [ebp-198h]  
00F66CC8  push        ecx  
00F66CC9  lea         ecx,[a]  
00F66CCC  call        A::operator= (0F61136h)       ;; assign to var a in main()
00F66CD1  mov         byte ptr [ebp-4],3  
00F66CD5  lea         ecx,[ebp-18Ch]  
00F66CDB  call        A::~A (0F612A8h)