为什么按价值参数排除在NRVO之外?
想像:
S f(S a) { return a; }
为什么不允许别名和返回值插槽?
S s = f(t); S s = t; // can't generally transform it to this :(
如果S
的拷贝构造函数有副作用,spec不允许这个转换。 相反,它需要至less两个副本(一个从t
到a
,一个从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
参数a
, bar
( $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?)
现在,写这样的类实际上可能是一个坏主意,但编译器的工作不是编译器的工作,只是为了确保输出是正确的和一致的。