这个c ++代码示例是否发生了两次复制构造函数调用?

时间:2014-01-21 23:46:22

标签: c++

方法:

Object test(){
    Object str("123");
    return str;
}

然后,我有两种方法可以调用它:

代码1:

const Object &object=test();

代码2:

Object object=test();

哪一个更好?如果没有优化,会在代码2中发生两次复制构造函数调用吗?

其他有什么区别?

对于code2,我认为

Object tmp=test();
Object object=tmp;
对于code1,我认为

Object tmp=test();
Object &object=tmp;

但是tmp将是方法之后的解构函数。所以它必须添加const?

是没有任何问题的代码1吗?

4 个答案:

答案 0 :(得分:4)

在C ++ 11中,std::string有一个移动构造函数/移动赋值运算符,因此代码为:

string str = test();

将(最坏的情况下)有一个构造函数调用和一个移动赋值调用。

即使没有移动语义,这也可能(可能)通过NRVO(返回值优化)进行优化。

基本上不要害怕按价值回归。

编辑:只是为了让它100%清楚发生了什么:

#include <iostream>
#include <string>

class object
{
    std::string s;

public:

    object(const char* c) 
      : s(c)
    {
        std::cout << "Constructor\n";
    }

    ~object() 
    {
        std::cout << "Destructor\n";
    }

    object(const object& rhs)
      : s(rhs.s)
    {
        std::cout << "Copy Constructor\n";
    }

    object& operator=(const object& rhs)
    {
        std::cout << "Copy Assignment\n";
        s = rhs.s;
        return *this;
    }

    object& operator=(object&& rhs)
    {
        std::cout << "Move Assignment\n";
        s = std::move(rhs.s);
        return *this;
    }

    object(object&& rhs)
      : s(std::move(rhs.s))
    {
        std::cout << "Move Constructor\n";
    }
};

object test()
{
    object o("123");
    return o;
}

int main()
{
    object o = test();
    //const object& o = test();
}

你可以看到每个都有1个构造函数调用和1个析构函数调用 - NRVO在这里(正如预期的那样)启动复制/移动。

答案 1 :(得分:4)

两个示例都有效 - 在1个const引用中引用一个临时对象,但此对象的生命周期会延长,直到引用超出范围(请参阅http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/)。第二个例子显然是有效的,而且大多数现代编译器都会优化掉额外的复制(如果使用C + 11移动语义则更好),所以出于实际目的,示例是等效的(尽管在2中你可以修改它)。

答案 2 :(得分:4)

让我们分析一下你的功能:

Object test()
{
    Object temp("123");
    return temp;
}

在这里,您要构建一个名为temp的局部变量,并从函数中返回它。 test()的返回类型为Object,表示您按值返回。按值返回局部变量是一件好事,因为它允许使用称为返回值优化(RVO)的特殊优化技术。会发生的是,编译器将忽略该调用并直接将初始化器构造到调用者的地址中,而不是调用对复制或移动构造函数的调用。在这种情况下,因为temp有一个名字(是一个左值),我们称之为N(amed)RVO。

假设发生了优化,尚未执行任何复制或移动。这就是你从main调用函数的方法:

int main()
{
    Object obj = test();
}

主要的第一行似乎特别值得关注,因为您认为临时将在完整表达式结束时被销毁。我认为这是一个令人担忧的原因,因为您认为obj不会被分配给有效对象,并且使用对const的引用初始化它是一种保持活着的方法。

你是对的两件事:

  • 临时表将在完整表达结束时销毁
  • 通过引用const对其进行初始化将延长其使用寿命

但临时将被销毁的事实并不令人担忧。因为初始化程序是一个右值,所以它的内容可以从移动

Object obj = test(); // move is allowed here

在copy-elision中进行保理,编译器将忽略对复制或移动构造函数的调用。因此,obj将被初始化为“好像”调用了复制或移动构造函数。因此,由于这些编译器优化,我们几乎没有理由担心多个副本。


但是如果我们接受你的其他例子呢?如果我们将obj限定为:

,该怎么办?
Object const& obj = test();

test()返回Object类型的prvalue。这个prvalue通常会在包含它的完整表达式的末尾被破坏,但由于它被初始化为const的引用,它的生命周期将扩展到引用的生命周期。

此示例与上一个示例有何区别?:

  • 您无法修改obj
  • 的状态
  • 禁止移动语义

如果您不熟悉移动语义,那么第一个要点是明显的,但不是第二个。因为objconst的引用,所以它无法移动,编译器无法利用有用的优化。将对const的引用分配给右值仅在一小部分情况下有用(如DaBrain指出的那样)。相反,最好是在有意义的时候运用值语义并创建值类型的对象。

此外,您甚至不需要函数test(),您只需创建对象:

Object obj("123");

但如果您确实需要test(),则可以利用类型扣除并使用auto

auto obj = test();

您的上一个示例涉及左值参考:

  

[..]但是tmp将在方法后被破坏。我们必须添加const吗?

Object &object = tmp;

在方法之后调用tmp的析构函数。考虑到我上面所说的内容,tmp被初始化的临时值将被移入tmp(或者它将被省略)。 tmp本身在超出范围之前不会破坏。所以不,不需要使用const

但是如果你想通过其他变量来引用tmp,那么引用是好的。否则,如果您知道之后不需要tmp,则可以从中移除:

Object object = std::move(tmp);

答案 3 :(得分:1)

代码1是正确的。正如我所说,C ++标准保证临时的const引用是有效的。它的主要用途是具有参考的多态行为:

#include <iostream>

class Base { public: virtual void Do() const { std::cout << "Base"; } };
class Derived : public Base { public: virtual void Do() const { std::cout << "Derived"; } };

Derived Factory() { return Derived(); }

int main(int argc, char **argv)
{
    const Base &ref = Factory();

    ref.Do();

    return 0;
}

这将返回“Derived”。一个着名的例子是Andrei Alexandrescu的ScopeGuard,但是使用C ++ 11它甚至更简单。