为什么按价值参数排除在NRVO之外?

想像:

S f(S a) { return a; } 

为什么不允许别名和返回值插槽?

 S s = f(t); S s = t; // can't generally transform it to this :( 

如果S的拷贝构造函数有副作用,spec不允许这个转换。 相反,它需要至less两个副本(一个从ta ,一个从a到返回值,另一个从返回值到s ,只有最后一个可以被忽略。代表t到f的拷贝的事实,唯一的拷贝在移动/拷贝构造函数的副作用的存在下仍然是强制性的)。

这是为什么?

这就是为什么copy elision对参数没有意义。 这实际上是关于在编译器级别实现这个概念的。

复制elision通过本质上构build返回值就地工作。 该值不会被复制出来; 它是直接在预定目的地创build的。 呼叫者为预期的输出提供空间,因此最终呼叫者提供了可能性。

函数内部需要做的所有的function是在调用者提供的地方构造输出。 如果该function可以做到这一点,你会得到复制elision。 如果函数不能,那么它将使用一个或多个临时variables来存储中间结果,然后将其复制/移动到调用者提供的位置。 它仍然在原地build造,但输出的结构通过复制发生。

所以,一个特定function之外的世界并不需要知道或关心一个function是否有效。 具体来说,函数的调用者不必知道函数是如何实现的。 没有什么不同, 这是决定是否有可能的function本身。

存储值参数也是由调用者提供的。 当你调用f(t) ,调用者创buildt的副本并将其传递给f 。 类似地,如果S是从int隐式构造的,则f(5)将从f(5)构造一个S并将其传递给f

这全部由调用者完成。 被调用者不知道或不在意它是一个variables还是一个临时的; 它只是给了堆栈内存(或寄存器或其他)的地方。

现在请记住:复制elision工作,因为被调用的函数直接构造variables到输出位置。 所以如果你试图从一个值参数中退出,那么value参数的存储也必须是输出存储本身 。 但请记住: 调用者为参数和输出提供了存储空间。 因此,为了避免输出副本, 调用者必须将参数直接构造到输出中

为此,调用者现在需要知道它所调用的函数将会返回返回值,因为如果参数将被返回,它只能将参数直接粘贴到输出中。 这在编译器级别通常是不可能的,因为调用者不一定具有该函数的实现。 如果函数内联,那么也许它可以工作。 但否则没有。

因此,C ++委员会并没有考虑到这个可能性。

就我所知,这个限制的基本原理是,调用约定可能(并且在很多情况下)会要求函数和返回对象的参数位于不同的位置(内存或寄存器)。 考虑下面的修改示例:

 X foo(); X bar( X a ) { return a; } int main() { X x = bar( foo() ); } 

从理论上讲,整套副本将是foo$tmp1 )中的return语句, bar参数abar$tmp2 )的返回语句以及main x 。 编译器可以通过在x的位置处创build$tmp1$tmp2来删除四个对象中的两个。 当编译器正在处理main ,可以注意到foo的返回值是bar的参数,并且可以使它们重合,在这一点上它不可能知道(没有内联) bar的参数和返回是同一个对象,它必须遵守调用约定,所以它会把$tmp1放在参数的位置上。

同时,它知道$tmp2的目的只是创buildx ,所以它可以放在同一个地址。 在bar ,没有太多可以做的事情:根据调用约定,参数a位于第一个参数的位置,并且$tmp2必须按照调用约定来定位(在一般情况下在a不同的位置,认为该示例可以扩展到一个bar ,需要更多的参数,其中只有一个被用作返回语句。

现在,如果编译器执行内联,它可以检测到如果函数未被内联的额外副本是真的不需要,它将有机会去除它。 如果标准允许删除特定的副本,那么根据函数是否内联,相同的代码将会有不同的行为。

从t到a是不合理的。 该参数被声明为可变的,所以复制完成,因为它被期望在函数中被修改。

从一个返回值我看不出任何复制的原因。 也许这是某种疏忽? 按值参数感觉像function体内的本地人…我看不出有什么区别。

大卫·罗德里格斯 – dribeas回答我的问题“如何允许C ++类的复制elisionbuild设”给了我下面的想法。 诀窍是使用lambdas在函数体内延迟评估:

 #include <iostream> struct S { S() {} S(const S&) { std::cout << "Copy" << std::endl; } S(S&&) { std::cout << "Move" << std::endl; } }; S f1(S a) { return a; } S f2(const S& a) { return a; } #define DELAY(x) [&]{ return x; } template <class F> S f3(const F& a) { return a(); } int main() { S t; std::cout << "Without delay:" << std::endl; S s1 = f1(t); std::cout << "With delay:" << std::endl; S s2 = f3(DELAY(t)); std::cout << "Without delay pass by ref:" << std::endl; S s3 = f2(t); std::cout << "Without delay pass by ref (temporary) (should have 0 copies, will get 1):" << std::endl; S s4 = f2(S()); std::cout << "With delay (temporary) (no copies, best):" << std::endl; S s5 = f3(DELAY(S())); } 

ideone GCC 4.5.1上的这个输出:

不延误:
复制
复制
延迟:
复制

现在,这是好的,但可以build议DELAY版本就像通过const引用传递,如下所示:

没有延迟通过ref:
复制

但是如果我们通过const引用传递一个临时对象,我们仍然得到一个副本:

没有延迟传递(临时)(应该有0份,将得到1):
复制

延迟版本在副本中的位置:

延迟(临时)(没有副本,最好):

正如你所看到的,这会暂时避免所有副本。

延迟版本在非临时情况下产生一个副本,在临时情况下不产生副本。 我不知道有什么办法可以达到这个目的,但如果有的话,我会很感兴趣。

我觉得,因为替代总是可用于优化:

 S& f(S& a) { return a; } // pass & return by reference ^^^ ^^^ 

如果你的例子中提到的f()被编码,那么假设复制是预期的或者副作用是完全可以的。 否则为什么不select通过参考?

假设如果NRVO适用(如你所问)那么S f(S)S& f(S&)之间没有区别!

NRVO在operator +() ( 例子 )的情况下踢,因为没有值得的select。

一个支持方面,所有下面的函数都有不同的复制行为:

 S& f(S& a) { return a; } // 0 copy S f(S& a) { return a; } // 1 copy S f(S a) { A a1; return (...)? a : a1; } // 2 copies 

在第三个代码片段中,如果(...)在编译时是已知的,那么编译器只生成一个副本。
这意味着,编译器有目的地不会执行优化时,一个微不足道的select是可用的。

我认为这个问题是,如果拷贝构造函数做了一些事情,那么编译器必须把这个事情做一个可预测的次数。 例如,如果您有一个类在每次复制时递增一个计数器,并且有一个访问该计数器的方法,那么符合标准的编译器必须执行该操作的定义次数(否则,将如何写入unit testing?)

现在,写这样的类实际上可能是一个坏主意,但编译器的工作不是编译器的工作,只是为了确保输出是正确的和一致的。