7

最近,我一直在阅读这篇文章以及建议停止返回 const 对象的文章。Stephan T. Lavavej在 Going Native 2013的演讲中也给出了这个建议。

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

  • 返回 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

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

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

我在这里想念什么?

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

4

4 回答 4

4

我不明白的是:为什么在“构造函数 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无关紧要,编译器根据 [conceptual 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;
}

这应该会触发复制构造,但是它又很简单,如果编译器认为它合适,它可能会完全忽略复制。

于 2013-12-12T14:55:45.160 回答
1

据我观察,移动构造函数优先于复制构造函数。正如 Yakk 所说,由于有多个返回路径,您不能省略移动构造函数。

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

右值将更喜欢右值引用。左值将更喜欢左值引用。相对于 r/l 值转换,CV 资格转换被认为是次要的。右值仍然可以绑定到 const 左值引用 (const A&),但前提是重载集中没有更有吸引力的右值引用。左值可以绑定到右值引用,但如果它存在于重载集中,则更喜欢左值引用。更多 cv 限定的对象不能绑定到更少 cv 限定的引用的规则代表......对于左值和右值引用。

此时可以进行进一步的语言改进。从函数返回具有自动存储功能的非 cv 限定对象时,应该有一个隐式转换为右值:

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 &参数中的 怎么办?它仍然会调用移动构造函数,但会调用参数的复制构造函数。如果你返回一个 const 对象呢?它将调用局部变量的复制构造函数。如果你返回一个const &? 它还将调用复制构造函数。

于 2013-12-12T14:18:26.590 回答
1

The answer is that your A sum local variable is being moved into the const A returned by the function (this is the Move Constructor output) and then the copy from the returned value into A acsum is being elided by the compiler (so there is no Copy Constructor output).

于 2013-12-12T14:20:40.187 回答
1

我反汇编了编译后的二进制文件(VC12 release build,O2),我的结论是:

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

move操作不能movecv 限定变量,但是之前从 中返回csum,该变量sum仍然是非常量变量,所以可以moved;并需要moved在返回后以备后用。

const修饰符只是禁止编译器在move返回后,但不禁止move在里面csum。如果删除constfrom csum,结果将是:

Default Constructor
Move Constructor
Destructor
Move Operator
Destructor

顺便说一句,您的测试程序有一个错误会导致a = sum(a, b);不正确,A 的默认 ctor 应该是:

A() : m(3) { println("    Default Constructor"); }

或者你会发现你给定的输出很难解释a = sum(a, b);


下面我将尝试分析调试构建 ASM。结果是一样的。(分析发布版本就像自杀 >_< )

主要的:

  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) 

总和:

  if (l.m == 0) {
00F665AA  mov         eax,dword ptr [l]  
00F665AD  cmp         dword ptr [eax],0  
00F665B0  jne         csum+79h (0F665D9h)  
    return r;
00F665B2  mov         eax,dword ptr [r]  
00F665B5  push        eax                            ;; r pushed as param for \
00F665B6  mov         ecx,dword ptr [ebp+8]  
00F665B9  call        A::A (0F613F2h)                ;; copy ctor of A
00F665BE  mov         dword ptr [ebp-4],0  
00F665C5  mov         ecx,dword ptr [ebp-0E4h]  
00F665CB  or          ecx,1  
00F665CE  mov         dword ptr [ebp-0E4h],ecx  
00F665D4  mov         eax,dword ptr [ebp+8]  
00F665D7  jmp         csum+0EEh (0F6664Eh)  
  }
  if (r.m == 0) {
00F665D9  mov         eax,dword ptr [r]  
00F665DC  cmp         dword ptr [eax],0  
00F665DF  jne         csum+0A8h (0F66608h)  
    return l;
00F665E1  mov         eax,dword ptr [l]  
00F665E4  push        eax                             ;; l pushed as param for \
00F665E5  mov         ecx,dword ptr [ebp+8]  
00F665E8  call        A::A (0F613F2h)                 ;; copy ctor of A
00F665ED  mov         dword ptr [ebp-4],0  
00F665F4  mov         ecx,dword ptr [ebp-0E4h]  
00F665FA  or          ecx,1  
00F665FD  mov         dword ptr [ebp-0E4h],ecx  
00F66603  mov         eax,dword ptr [ebp+8]  
00F66606  jmp         csum+0EEh (0F6664Eh)  
  }
  A sum;
00F66608  lea         ecx,[sum]  
  A sum;
00F6660B  call        A::A (0F61244h)                  ;; ctor of result sum
00F66610  mov         dword ptr [ebp-4],1  
  sum.m = l.m + r.m;
00F66617  mov         eax,dword ptr [l]  
00F6661A  mov         ecx,dword ptr [eax]  
00F6661C  mov         edx,dword ptr [r]  
00F6661F  add         ecx,dword ptr [edx]  
00F66621  mov         dword ptr [sum],ecx  
  return sum;
00F66624  lea         eax,[sum]  
00F66627  push        eax                              ;; sum pushed as param for \
00F66628  mov         ecx,dword ptr [ebp+8]  
00F6662B  call        A::A (0F610D2h)                  ;; move ctor of A (this one is pushed in main as a temp variable on stack)
00F66630  mov         ecx,dword ptr [ebp-0E4h]  
00F66636  or          ecx,1  
00F66639  mov         dword ptr [ebp-0E4h],ecx  
00F6663F  mov         byte ptr [ebp-4],0  
00F66643  lea         ecx,[sum]  
00F66646  call        A::~A (0F612A8h)                 ;; dtor of sum
00F6664B  mov         eax,dword ptr [ebp+8]  
}
于 2013-12-12T14:34:40.017 回答