想象:
S f(S a) {
return a;
}
为什么不允许别名a
和返回值插槽?
S s = f(t);
S s = t; // can't generally transform it to this :(
如果S
的复制构造函数具有副作用,则规范不允许此转换。相反,它至少需要两个副本(一个从t
到a
,一个从a
到返回值,另一个从返回值到s
,只有最后一个才能被删除。请注意,我在上面写了= t
来表示t
到f的a
副本的事实,这是唯一一个在现场仍然是强制性的副本移动/复制构造函数的副作用)。
为什么?
答案 0 :(得分:19)
这就是为什么复制省略对参数没有意义的原因。它实际上是关于编译器级别的概念的实现。
复制elision的工作原理是基本上构建返回值。该值不会被复制出来;它直接在预定目的地创建。呼叫者为预期的输出提供空间,因此最终是呼叫者提供了省略的可能性。
函数内部需要执行的所有操作都是为了忽略副本,而是在调用者提供的位置构造输出。如果函数可以执行此操作,则会获得copy elision。如果函数不能,则它将使用一个或多个临时变量来存储中间结果,然后将其复制/移动到调用者提供的位置。它仍然就地构建,但输出的构造通过复制发生。
因此,特定功能之外的世界不必知道或关心某个功能是否能够完成。具体来说,函数的调用者不必了解函数的实现方式。它没有做任何不同的事情;这是函数本身决定是否可以省略。
调用者也提供值参数的存储。当您致电f(t)
时,调用者会创建t
的副本并将其传递给f
。同样,如果S
可以隐式构建int
,那么f(5)
将从5构建S
并将其传递给f
。
这完全由调用者完成。被叫者不知道或不关心它是变量还是临时的;它只是给出了一堆堆栈内存(或寄存器或其他)。
现在请记住:copy elision有效,因为被调用的函数将变量直接构造到输出位置。因此,如果您试图忽略值参数的返回,则值参数的存储也必须是输出存储本身。但请记住:调用者为参数和输出提供了存储空间。因此,为了省略输出副本,调用者必须将参数直接构造到输出。
要做到这一点,现在调用者需要知道它调用的函数将忽略返回值,因为如果返回参数,它只能将参数直接粘贴到输出中。这在编译器级别通常是不可能的,因为调用者不一定具有该函数的实现。如果函数是内联的,那么也许它可以工作。但否则没有。
因此,C ++委员会并不愿意考虑这种可能性。
答案 1 :(得分:3)
根据我的理解,这个限制的基本原理是调用约定可能(并且在许多情况下)要求函数和返回对象的参数位于不同的位置(存储器或寄存器)。请考虑以下修改示例:
X foo();
X bar( X a )
{
return a;
}
int main() {
X x = bar( foo() );
}
理论上,整套副本将是foo
($tmp1
)中的return语句,a
的{{1}}参数,bar
的返回语句( bar
中的$tmp2
)和x
。编译器可以通过在main
和$tmp1
位置a
处创建$tmp2
来忽略四个对象中的两个。当编译器处理x
时,它可以注意到main
的返回值是foo
的参数并且可以使它们重合,此时它不可能知道(没有内联) bar
的参数和返回是同一个对象,它必须符合调用约定,因此它将bar
置于$tmp1
的参数位置。
同时,它知道bar
的目的只是创建$tmp2
,因此它可以将两者放在同一地址。在x
内部,没有太多可以做的事情:根据调用约定,参数bar
位于第一个参数的位置,a
必须根据调用约定(在一般情况下,在不同的位置,认为该示例可以扩展为带有更多参数的$tmp2
,其中只有一个用作返回语句。
现在,如果编译器执行内联,它可以检测到实际上不需要内联函数时所需的额外副本,并且它有可能将其删除。如果标准允许省略该特定副本,则相同的代码将具有不同的行为,具体取决于函数是否内联。
答案 2 :(得分:1)
从t到a,删除副本是不合理的。该参数被声明为可变,因此复制完成是因为它应该在函数中被修改。
从a到返回值,我看不到任何复制的理由。也许是某种疏忽? by-value参数感觉就像函数体内的本地人一样...我认为没有区别。
答案 3 :(得分:1)
DavidRodríguez - 我的问题'How to allow copy elision construction for C++ classes'的问题回答给了我以下想法。诀窍是使用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:
毫不拖延:
复制
复制
延迟:
复制
现在这很好,但可以建议DELAY版本就像传递const引用一样,如下所示:
没有延迟通过ref:
复制
但是如果我们传递一个临时的const引用,我们仍然得到一个副本:
没有延迟通过ref(临时)(应该有0份副本,将获得1):
复制
延迟版本省略副本:
延迟(临时)(无副本,最佳):
正如你所看到的,这就是所有临时案件中的副本。
延迟版本在非临时案例中生成一个副本,在临时案例中不生成副本。我不知道除了lambdas之外还有什么方法可以实现这个目标,但如果有的话,我会感兴趣。
答案 4 :(得分:0)
我觉得,因为替代方案始终可用进行优化:
S& f(S& a) { return a; } // pass & return by reference
^^^ ^^^
如果f()
按照您的示例中的说明进行编码,则可以认为副本是或预期会产生副作用;否则为什么不选择通过/返回参考?
假设NRVO适用(如您所知),那么S f(S)
和S& f(S&)
之间没有区别!
NRVO在operator +()
(example)等情况下开始,因为没有值得替代的选择。
一个支持方面,所有以下功能都有不同的复制行为:
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
在第3个片段中,如果(...)
在编译时已知为false
,那么编译器只生成1个副本。
这意味着,当一个简单的替代方案可用时,该编译器有目的地不执行优化。
答案 5 :(得分:-1)
我认为问题是如果复制构造函数做了某些事情,那么编译器必须做出可预测次数的事情。例如,如果你有一个类在每次复制时递增一个计数器,并且有一种方法可以访问该计数器,那么符合标准的编译器必须执行定义次数的操作(否则,如何写一个单元测试?)
现在,实际编写这样的类可能是一个坏主意,但编译器的工作并不是编译器的工作,只是为了确保输出正确且一致。