5

如果我有一个 A 类(它按值返回一个对象),并且两个函数 f() 和 g() 仅在它们的返回变量上有所不同:

class A
{
    public:
    A () { cout<<"constructor, "; }
    A (const A& ) { cout<<"copy-constructor, "; }
    A& operator = (const A& ) { cout<<"assignment, "; }
    ~A () { cout<<"destructor, "; }
};
    const A f(A x)
    {A y; cout<<"f, "; return y;}

    const A g(A x)
    {A y; cout<<"g, "; return x;}

main()
{
    A a;
    A b = f(a);
    A c = g(a);
}

现在当我执行 line 时A b = f(a);,它​​输出:

copy-constructor, constructor, f, destructor, 假设 f() 中的对象 y 是直接在目标(即对象 b 的内存位置)创建的,并且不涉及临时对象,这很好。

当我执行 line 时A c = g(a);,它​​输出:

copy-constructor, constructor, g, copy-constructor, destructor, destructor,.

所以问题是为什么在 g() 的情况下不能直接在 c 的内存位置创建对象,就像调用 f() 时发生的那样?为什么它在第二种情况下调用一个额外的复制构造函数(我认为这是因为临时的参与)?

4

4 回答 4

7

问题在于,在第二种情况下,您将返回其中一个参数。鉴于通常参数复制发生在调用者的位置,而不是在函数内(main在这种情况下),编译器会进行复制,然后在进入g().

来自http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

其次,我还没有找到一个编译器可以在返回函数参数时忽略副本,就像我们的 sorted 实现一样。当您考虑这些省略是如何完成的时,这是有道理的:如果没有某种形式的过程间优化, sorted 的调用者无法知道最终将返回参数(而不是其他对象),因此编译器必须在堆栈上为参数和返回值分配单独的空间。

于 2012-06-06T13:26:36.373 回答
4

这是对您的代码的一些修改,它将帮助您完全理解那里发生的事情:

class A{
public:
    A(const char* cname) : name(cname){
        std::cout << "constructing " << cname << std::endl;
    }
    ~A(){
        std::cout << "destructing " << name.c_str() << std::endl;
    }
    A(A const& a){
        if (name.empty()) name = "*tmp copy*";
        std::cout 
            << "creating " << name.c_str() 
            << " by copying " << a.name.c_str() << std::endl;
    }
    A& operator=(A const& a){
        std::cout
            << "assignment ( "
                << name.c_str() << " = " << a.name.c_str()
            << " )"<< std::endl;
        return *this;
    }
    std::string name;
};

下面是这个类的用法:

const A f(A x){
    std::cout 
        << "// renaming " << x.name.c_str() 
        << " to x in f()" << std::endl;
    x.name = "x in f()";
    A y("y in f()");
    return y;
}

const A g(A x){
    std::cout 
        << "// renaming " << x.name.c_str()
        << " to x in f()" << std::endl;
    x.name = "x in g()";
    A y("y in g()");
    return x;
}

int main(){
    A a("a in main()");
    std::cout << "- - - - - - calling f:" << std::endl;
    A b = f(a);
    b.name = "b in main()";
    std::cout << "- - - - - - calling g:" << std::endl;
    A c = g(a);
    c.name = "c in main()";
    std::cout << ">>> leaving the scope:" << std::endl;
    return 0;
}

这是在没有任何优化的情况下编译时的输出:

constructing a in main()
- - - - - - calling f:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in f()
creating *tmp copy* by copying y in f()
destructing y in f()
destructing x in f()
- - - - - - calling g:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in g()
creating *tmp copy* by copying x in g()
destructing y in g()
destructing x in g()
>>> leaving the scope:
destructing c in main()
destructing b in main()
destructing a in main()

您发布的输出是使用Named Return Value Optimization编译的程序的输出。在这种情况下,编译器会尝试消除冗余的 Copy 构造函数和 Destructor 调用,这意味着在返回对象时,它将尝试返回对象而不创建其冗余副本。这是启用 NRVO 的输出:

constructing a in main()
- - - - - - calling f:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in f()
destructing x in f()
- - - - - - calling g:
creating *tmp copy* by copying a in main()
// renaming *tmp copy* to x in f()
constructing y in g()
creating *tmp copy* by copying x in g()
destructing y in g()
destructing x in g()
>>> leaving the scope:
destructing c in main()
destructing b in main()
destructing a in main()

在第一种情况下,由于 NRVO 已完成其工作,因此不会创建*tmp copy*通过复制。y in f()在第二种情况下,由于在此函数中声明了另一个返回槽的候选者,因此无法应用 NRVO。有关更多信息,请参阅:C++:使用“return”语句避免复制:)

于 2012-06-06T13:37:46.717 回答
3

不同之处在于,在这种g情况下,您将返回一个传递给函数的值。该标准明确规定了在哪些条件下可以在 12.8p31 中删除副本,并且不包括从函数参数中删除副本。

基本上问题是参数的位置和返回的对象是由调用约定固定的,编译器无法根据实现(甚至可能在调用位置不可见)返回的事实来更改调用约定论点。

前段时间我开了一个短暂的博客(我希望有更多的时间......),我写了几篇关于 NRVO 和复制省略的文章,这可能有助于澄清这一点(或者不是,谁知道 :)):

值语义:NRVO

值语义:复制省略

于 2012-06-06T13:55:22.310 回答
0

它可以(几乎)优化整个 g() 函数调用,在这种情况下,您的代码如下所示:

A a;
A c = a;

实际上,这就是您的代码正在做的事情。现在,当您a作为按值参数(即不是引用)传递时,编译器几乎必须在那里执行一个复制,然后它按值返回这个参数,它必须执行另一个复制。

在 f() 的情况下,因为它将实际上是一个临时的东西返回到一个未初始化的变量中,编译器可以看到它c用作 f() 内部变量的存储是安全的。

于 2012-06-06T13:55:43.503 回答