在此C 代码示例中发生了两次复制构造函数的调用

is twice calls to copy constructor happen for this c++ code sample?

本文关键字:两次 复制 构造函数 调用 代码 发生了 在此      更新时间:2023-10-16

用于方法:

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是否没有任何问题?

让我们分析您的功能:

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

在这里,您正在构建一个名为 temp的本地变量,并将其从函数返回。test()的返回类型是Object,这意味着您是按值返回的。按值返回本地变量是一件好事,因为它允许进行一种称为返回值优化(RVO)的特殊优化技术。发生的事情是,编译器将不用调用副本或移动构造函数,而是将呼叫的编译器直接构造到呼叫者的地址中。在这种情况下,由于temp具有名称(是lvalue),因此我们称其为n(amed)rvo。

假设进行了优化,尚未执行副本或移动。这就是您从main调用函数的方式:

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

Main中的第一行似乎对您特别关注,因为您认为临时性将在完整表达结束时被摧毁。我假设这是引起关注的原因,因为您认为obj不会被分配给有效的对象,并且用引用const初始化它是一种使其生存的方法。

您对两件事是正确的:

  • 临时性将在完整表达结束时被摧毁
  • const的引用将其初始化将延长其寿命

但是,临时性将被摧毁的事实并不是引起人们关注的原因。因为初始化器是rvalue,所以它的内容可以从。

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

分解复制仪,编译器将呼叫到副本或移动构造函数。因此,obj将被初始化,就像if if'副本或移动构造函数被调用。因此,由于这些编译器的优化,我们几乎没有理由害怕多个副本。


但是,如果我们招待您的其他例子怎么办?相反,我们有合格的obj为:

Object const& obj = test();

test()返回Object类型的prvalue。通常会在包含其包含的完整表达式的末尾破坏此prvalue,但是由于将其初始化为对const的引用,因此其寿命延长到参考文献。

这个示例与上一个示例之间有什么区别?:

  • 您无法修改obj的状态
  • 它抑制移动语义

第一个子弹点很明显,但如果您不熟悉移动语义,则不是第二点。因为obj是对const的引用,所以它不能从中移动,并且编译器无法利用有用的优化。将对const的引用分配给RVALUE仅在一组狭窄的情况下有用(正如Dabrain指出的那样)。相反,最好是您锻炼价值仪表并在有意义的情况下创建价值类型的对象。

此外,您甚至不需要函数test(),您可以简单地创建对象:

Object obj("123");

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

auto obj = test();

您的最后一个示例涉及lvalue-reference:

[..],但是tmp将在方法之后被破坏。因此,我们必须添加const

Object &object = tmp;

tmp的破坏者是未在方法后调用的。考虑到我上面所说的话,将tmp初始化的临时性将移至tmp(或将其省用)。tmp本身不会破坏它,直到它消失了。所以不,无需使用const

但是,如果您想通过其他一些变量参考tmp,则可以参考。否则,如果您知道之后不需要tmp,则可以从中移动:

Object object = std::move(tmp);

您的两个示例都是有效的 - 1 const引用是指临时对象,但是该对象的寿命延长了,直到参考范围不范围(请参阅http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/)。第二个示例显然是有效的,大多数现代编译器都会优化额外的复制(如果您使用C 11 Move语义),因此出于实际目的,示例是等效的(尽管在2中,您可以修改值)。

在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 << "Constructorn";
    }
    ~object() 
    {
        std::cout << "Destructorn";
    }
    object(const object& rhs)
      : s(rhs.s)
    {
        std::cout << "Copy Constructorn";
    }
    object& operator=(const object& rhs)
    {
        std::cout << "Copy Assignmentn";
        s = rhs.s;
        return *this;
    }
    object& operator=(object&& rhs)
    {
        std::cout << "Move Assignmentn";
        s = std::move(rhs.s);
        return *this;
    }
    object(object&& rhs)
      : s(std::move(rhs.s))
    {
        std::cout << "Move Constructorn";
    }
};
object test()
{
    object o("123");
    return o;
}
int main()
{
    object o = test();
    //const object& o = test();
}

您可以看到每个构造函数调用和1个destructor调用 - NRVO在此处启动(如预期的),将复制/移动射出。

代码1是正确的。正如我所说,C 标准可以保证临时参考的临时性是有效的。主要用途是富含浓度的多态性行为:

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

这将返回"派生"。一个著名的例子是Andrei Alexandrescu的ScopeGuard,但使用C 11,这甚至更简单。