【发布时间】:2016-10-28 20:08:55
【问题描述】:
在 2016 年奥卢 ISO C++ 标准会议上,一项名为 Guaranteed copy elision through simplified value categories 的提案被标准委员会投票通过了 C++17。
保证复制省略究竟是如何工作的?它是否涵盖了一些已经允许复制省略的情况,或者是否需要更改代码来保证复制省略?
【问题讨论】:
标签: c++ c++17 copy-elision
在 2016 年奥卢 ISO C++ 标准会议上,一项名为 Guaranteed copy elision through simplified value categories 的提案被标准委员会投票通过了 C++17。
保证复制省略究竟是如何工作的?它是否涵盖了一些已经允许复制省略的情况,或者是否需要更改代码来保证复制省略?
【问题讨论】:
标签: c++ c++17 copy-elision
允许在多种情况下发生复制省略。然而,即使它被允许,代码仍然必须能够像没有删除副本一样工作。也就是说,必须有一个可访问的副本和/或移动构造函数。
保证复制省略重新定义了许多 C++ 概念,因此可以省略复制/移动的某些情况实际上根本不会引发复制/移动。编译器没有省略副本;标准规定永远不会发生这样的复制。
考虑这个函数:
T Func() {return T();}
在非保证复制省略规则下,这将创建一个临时文件,然后从该临时文件移动到函数的返回值。该移动操作可能被省略,但T 必须仍然有一个可访问的移动构造函数,即使它从未使用过。
同样:
T t = Func();
这是t 的复制初始化。这将使用返回值Func 复制初始化t。然而,T 仍然必须有一个移动构造函数,即使它不会被调用。
保证复制省略redefines the meaning of a prvalue expression。在 C++17 之前,prvalues 是临时对象。在 C++17 中,prvalue 表达式只是可以物化临时的东西,但它还不是临时的。
如果您使用纯右值来初始化纯右值类型的对象,则不会实现临时对象。当您执行return T(); 时,这将通过prvalue 初始化函数的返回值。由于该函数返回T,因此不会创建临时函数; prvalue的初始化只是直接初始化返回值。
要理解的是,由于返回值是纯右值,它还不是对象。它只是一个对象的初始化器,就像T() 一样。
做T t = Func();时,返回值的prvalue直接初始化对象t;没有“创建临时和复制/移动”阶段。由于Func()的返回值是等价于T()的prvalue,所以t直接由T()初始化,就像你已经完成T t = T()一样。
如果以任何其他方式使用纯右值,则纯右值将具体化一个临时对象,该对象将在该表达式中使用(如果没有表达式,则将其丢弃)。所以如果你做了const T &rt = Func();,prvalue 将实现一个临时的(使用T() 作为初始化器),它的引用将存储在rt,以及通常的临时生命周期扩展的东西。
保证省略允许您做的一件事是返回不可移动的对象。例如,lock_guard 不能被复制或移动,所以你不能有一个按值返回它的函数。但是通过保证复制省略,您可以。
保证省略也适用于直接初始化:
new T(FactoryFunction());
如果FactoryFunction 按值返回T,则此表达式不会将返回值复制到分配的内存中。它将改为分配内存并使用分配的内存作为函数调用的返回值内存。
所以按值返回的工厂函数可以直接初始化堆分配的内存而不用知道它。当然,只要这些函数在内部遵循保证复制省略的规则。他们必须返回 T 类型的纯右值。
当然,这也有效:
new auto(FactoryFunction());
如果你不喜欢写类型名。
重要的是要认识到上述保证仅适用于纯右值。也就是说,当返回 named 变量时,您无法保证:
T Func()
{
T t = ...;
...
return t;
}
在这种情况下,t 必须仍然具有可访问的复制/移动构造函数。是的,编译器可以选择优化复制/移动。但编译器仍必须验证可访问的复制/移动构造函数是否存在。
因此,命名返回值优化 (NRVO) 没有任何变化。
【讨论】:
std::function<T()> 真的。
我认为这里已经很好地分享了复制省略的细节。但是,我找到了这篇文章:https://jonasdevlieghere.com/guaranteed-copy-elision,它指的是 C++17 在返回值优化情况下保证复制省略。
它还指的是如何使用 gcc 选项:-fno-elide-constructors,可以禁用复制省略,并看到不是直接在目的地调用构造函数,而是看到 2 个复制构造函数(或在 c 中移动构造函数++11) 及其相应的析构函数被调用。以下示例显示了这两种情况:
#include <iostream>
using namespace std;
class Foo {
public:
Foo() {cout << "Foo constructed" << endl; }
Foo(const Foo& foo) {cout << "Foo copy constructed" << endl;}
Foo(const Foo&& foo) {cout << "Foo move constructed" << endl;}
~Foo() {cout << "Foo destructed" << endl;}
};
Foo fReturnValueOptimization() {
cout << "Running: fReturnValueOptimization" << endl;
return Foo();
}
Foo fNamedReturnValueOptimization() {
cout << "Running: fNamedReturnValueOptimization" << endl;
Foo foo;
return foo;
}
int main() {
Foo foo1 = fReturnValueOptimization();
Foo foo2 = fNamedReturnValueOptimization();
}
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 testFooCopyElision.cxx # Copy elision enabled by default
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo destructed
Foo destructed
vinegupt@bhoscl88-04(~/progs/cc/src)$ g++ -std=c++11 -fno-elide-constructors testFooCopyElision.cxx # Copy elision disabled
vinegupt@bhoscl88-04(~/progs/cc/src)$ ./a.out
Running: fReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Running: fNamedReturnValueOptimization
Foo constructed
Foo move constructed
Foo destructed
Foo move constructed
Foo destructed
Foo destructed
Foo destructed
我看到了返回值优化。即无论 c++ 17 是什么,通常都可以保证在返回语句中复制删除临时对象。
但是,返回的局部变量的命名返回值优化大多发生但不能保证。在具有不同返回语句的函数中,我看到如果每个返回语句返回本地范围的变量或相同范围的变量,它就会发生。否则,如果在不同的 return 语句中返回不同范围的变量,编译器将很难执行复制省略。
如果有一种方法可以保证复制省略或在无法执行复制省略时得到某种警告,这将使开发人员确保执行复制省略并在无法执行时重构代码,那就太好了'不会执行。
【讨论】: