【问题标题】:How actually does a function return by value?函数实际上是如何按值返回的?
【发布时间】:2012-06-10 11:50:36
【问题描述】:

如果我有一个类 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);
}

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

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

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

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

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

【问题讨论】:

  • 如果您希望编译器执行优化,那么您必须在启用优化的情况下进行编译。
  • 我认为这与编译器优化无关,因为我已经尝试过了。

标签: c++ temporary return-by-value


【解决方案1】:

这里对您的代码稍作修改,这将帮助您完全理解那里发生了什么:

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 已完成其工作,因此不会通过复制 y in f() 创建 *tmp copy*。在第二种情况下,虽然不能应用 NRVO,因为在这个函数中声明了另一个返回槽的候选者。欲了解更多信息,请参阅:C++ : Avoiding copy with the "return" statement :)

【讨论】:

  • 是的,我知道这一点,并且我已经在我的代码中完成了它以查看到底发生了什么(尽管我已经发布了代码的简化版本,仅强调我的问题是什么) .这段代码对我提出的问题毫无用处。我被问到的是正在发生的事情的原因,而不是发生的事情本身。无论如何,感谢您的关注:)
  • @cirronimbo:现在检查我的答案,它解释了启用 NRVO 的情况,还解释了为什么我建议你这个问题。
【解决方案2】:

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

A a;
A c = a;

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

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

【讨论】:

    【解决方案3】:

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

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

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

    Value semantics: NRVO

    Value semantics: Copy elision

    【讨论】:

    • 非常感谢。您的“[未]定义的行为”肯定以“定义明确”的方式解决了我的许多疑问 :) 但是,如果您能告诉他们,我还有更多疑问:1.在 NRVO 中,当您使用“布尔”时从“type x and y”中决定返回哪个“type”,似乎我的编译器无法进行省略(我也怀疑其他人。)
    • 2.在我的代码中,如果我将引用绑定到一个临时对象,即“ A & b = f(a); ”,那么会发生什么情况是对象的范围(假设是由 f(a) 返回的临时对象)会增加,直到 main 的末尾大括号.所以这是矛盾的两件事 - 1. 正如你在博客中提到的那样,获取临时地址是非法的,但我们正在这里做。 2. 临时怎么能持续这么久?
    • 3.是不是函数的本地成员对象的范围和它的参数的范围不同。就像我做“A a; A b; b = g(a);”之类的事情一样然后在“b = g(a)”行中,局部对象的析构函数在赋值运算符之前调用,参数的析构函数在赋值之后调用。
    • 对不起,由于篇幅限制,请把所有三个疑问写成一个(希望你能管理:))
    • 1.那是编译器依赖的。我不希望很多编译器能够接受它,但理论上是可能的(您是否尝试过最高优化级别?) 2. 标准中有一个明确的规则可以在绑定 const 引用时启用寿命延长,并且您获取地址,仅创建一个 reference(将引用视为别名),在这种特殊情况下,编译器可以从二进制文件中删除引用,而只是用原始对象的用途替换参考用途....
    【解决方案4】:

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

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

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

    【讨论】:

    • 我还没有找到一个编译器可以在返回函数参数时忽略副本——这并不奇怪,不可能有一个调用约定允许为此,标准(在撰写这篇文章之后很愉快)明确指出编译器无法做到这一点。
    猜你喜欢
    • 2012-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-23
    • 2021-07-07
    • 1970-01-01
    • 1970-01-01
    • 2015-01-27
    相关资源
    最近更新 更多