最近,我一直在阅读this postthat post,建议停止返回const对象。
Stephan T. Lavavej在《 Going Native 2013》的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;
    }
    

    这是 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;
    }
    

    这应该触发拷贝的构造,但是再一次,它很简单,只要编译器认为合适就可以完全删除拷贝。

    09-10 06:29
    查看更多