保证复制省略是如何工作的?
How does guaranteed copy elision work?
在 2016 年 Oulu ISO C++ 标准会议上,一项名为 Guaranteed copy elision through simplified value categories 的提案被标准委员会投票选入 C++17。
有保证的复制省略究竟是如何工作的?它是否涵盖了某些已经允许复制省略的情况,或者是否需要更改代码来保证复制省略?
在许多情况下允许复制省略。然而,即使它被允许,代码仍然必须能够像没有删除副本一样工作。也就是说,必须有一个可访问的副本 and/or 移动构造函数。
保证复制省略重新定义了一些 C++ 概念,这样 copies/moves 可能被省略的某些情况实际上根本不会引发 copy/move .编译器没有删除副本;该标准规定永远不会发生此类复制。
考虑这个函数:
T Func() {return T();}
根据无保证的复制省略规则,这将创建一个临时文件,然后从该临时文件移动到函数的 return 值中。该移动操作 可以 被省略,但是 T
必须仍然有一个可访问的移动构造函数,即使它从未被使用过。
同样:
T t = Func();
这是t
的复制初始化。这将使用 Func
的 return 值复制初始化 t
。但是,T
仍然必须有一个移动构造函数,即使它不会被调用。
保证复制省略 redefines the meaning of a prvalue expression。在 C++17 之前,纯右值是临时对象。在 C++17 中,纯右值表达式只是可以 实现 临时的东西,但它还不是临时的。
如果您使用纯右值来初始化纯右值类型的对象,则不会具体化任何临时对象。当您执行 return T();
时,这会通过纯右值初始化函数的 return 值。由于那个函数 returns T
,没有创建临时文件;纯右值的初始化只是直接初始化 return 值。
需要了解的是,由于 return 值是纯右值,因此它 还不是对象 。它只是一个对象的初始值设定项,就像 T()
一样。
做T t = Func();
时,return值的纯右值直接初始化对象t
;没有 "create a temporary and copy/move" 阶段。由于 Func()
的 return 值是等价于 T()
的纯右值,t
直接由 T()
初始化,就好像您已经完成了 T t = T()
.
如果以任何其他方式使用纯右值,纯右值将具体化一个临时对象,该对象将在该表达式中使用(如果没有表达式则丢弃)。因此,如果您执行 const T &rt = Func();
,纯右值将实现一个临时值(使用 T()
作为初始值设定项),其引用将存储在 rt
中,连同通常的临时生命周期扩展内容。
保证省略允许您做的一件事是 return 不动的对象。例如,lock_guard
无法复制或移动,因此您无法使用 return 按值编辑它的函数。但是通过保证复制省略,您可以。
保证省略也适用于直接初始化:
new T(FactoryFunction());
If FactoryFunction
returns T
by value,此表达式不会将return值复制到分配的内存中。它会分配内存并使用分配的内存作为直接调用函数的return值内存。
因此 return 按值的工厂函数可以直接初始化堆分配的内存,甚至不知道它。当然,只要这些函数 内部 遵循保证复制省略的规则。他们必须 return 一个 T
.
类型的纯右值
当然,这也行:
new auto(FactoryFunction());
如果你不喜欢写类型名。
重要的是要认识到上述保证仅适用于纯右值。也就是说,当 returning 一个 命名的 变量时,您无法得到保证:
T Func()
{
T t = ...;
...
return t;
}
在这种情况下,t
必须仍然有一个可访问的 copy/move 构造函数。是的,编译器可以选择优化掉 copy/move。但是编译器仍然必须验证可访问的 copy/move 构造函数的存在。
所以命名 return 值优化 (NRVO) 没有任何变化。
我认为复制省略的细节在这里得到了很好的分享。但是,我发现了这篇文章:https://jonasdevlieghere.com/guaranteed-copy-elision,它指的是在 return 值优化案例中 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
我看到 return 值优化。即return 语句中临时对象的复制省略通常得到保证,而不管 c++ 17.
但是,returned 局部变量的命名 return 值优化大多数情况下都会发生,但不能保证。在具有不同 return 语句的函数中,我看到如果 return 语句中的每个 returns 局部范围的变量或相同范围的变量都会发生。否则,如果在不同的 return 语句中对不同范围的变量进行 return 编辑,编译器将很难执行复制省略。
如果有一种方法可以保证复制省略或在无法执行复制省略时获得某种警告,这将让开发人员确保执行复制省略并重构代码,那就太好了无法执行。
在 2016 年 Oulu ISO C++ 标准会议上,一项名为 Guaranteed copy elision through simplified value categories 的提案被标准委员会投票选入 C++17。
有保证的复制省略究竟是如何工作的?它是否涵盖了某些已经允许复制省略的情况,或者是否需要更改代码来保证复制省略?
在许多情况下允许复制省略。然而,即使它被允许,代码仍然必须能够像没有删除副本一样工作。也就是说,必须有一个可访问的副本 and/or 移动构造函数。
保证复制省略重新定义了一些 C++ 概念,这样 copies/moves 可能被省略的某些情况实际上根本不会引发 copy/move .编译器没有删除副本;该标准规定永远不会发生此类复制。
考虑这个函数:
T Func() {return T();}
根据无保证的复制省略规则,这将创建一个临时文件,然后从该临时文件移动到函数的 return 值中。该移动操作 可以 被省略,但是 T
必须仍然有一个可访问的移动构造函数,即使它从未被使用过。
同样:
T t = Func();
这是t
的复制初始化。这将使用 Func
的 return 值复制初始化 t
。但是,T
仍然必须有一个移动构造函数,即使它不会被调用。
保证复制省略 redefines the meaning of a prvalue expression。在 C++17 之前,纯右值是临时对象。在 C++17 中,纯右值表达式只是可以 实现 临时的东西,但它还不是临时的。
如果您使用纯右值来初始化纯右值类型的对象,则不会具体化任何临时对象。当您执行 return T();
时,这会通过纯右值初始化函数的 return 值。由于那个函数 returns T
,没有创建临时文件;纯右值的初始化只是直接初始化 return 值。
需要了解的是,由于 return 值是纯右值,因此它 还不是对象 。它只是一个对象的初始值设定项,就像 T()
一样。
做T t = Func();
时,return值的纯右值直接初始化对象t
;没有 "create a temporary and copy/move" 阶段。由于 Func()
的 return 值是等价于 T()
的纯右值,t
直接由 T()
初始化,就好像您已经完成了 T t = T()
.
如果以任何其他方式使用纯右值,纯右值将具体化一个临时对象,该对象将在该表达式中使用(如果没有表达式则丢弃)。因此,如果您执行 const T &rt = Func();
,纯右值将实现一个临时值(使用 T()
作为初始值设定项),其引用将存储在 rt
中,连同通常的临时生命周期扩展内容。
保证省略允许您做的一件事是 return 不动的对象。例如,lock_guard
无法复制或移动,因此您无法使用 return 按值编辑它的函数。但是通过保证复制省略,您可以。
保证省略也适用于直接初始化:
new T(FactoryFunction());
If FactoryFunction
returns T
by value,此表达式不会将return值复制到分配的内存中。它会分配内存并使用分配的内存作为直接调用函数的return值内存。
因此 return 按值的工厂函数可以直接初始化堆分配的内存,甚至不知道它。当然,只要这些函数 内部 遵循保证复制省略的规则。他们必须 return 一个 T
.
当然,这也行:
new auto(FactoryFunction());
如果你不喜欢写类型名。
重要的是要认识到上述保证仅适用于纯右值。也就是说,当 returning 一个 命名的 变量时,您无法得到保证:
T Func()
{
T t = ...;
...
return t;
}
在这种情况下,t
必须仍然有一个可访问的 copy/move 构造函数。是的,编译器可以选择优化掉 copy/move。但是编译器仍然必须验证可访问的 copy/move 构造函数的存在。
所以命名 return 值优化 (NRVO) 没有任何变化。
我认为复制省略的细节在这里得到了很好的分享。但是,我发现了这篇文章:https://jonasdevlieghere.com/guaranteed-copy-elision,它指的是在 return 值优化案例中 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
我看到 return 值优化。即return 语句中临时对象的复制省略通常得到保证,而不管 c++ 17.
但是,returned 局部变量的命名 return 值优化大多数情况下都会发生,但不能保证。在具有不同 return 语句的函数中,我看到如果 return 语句中的每个 returns 局部范围的变量或相同范围的变量都会发生。否则,如果在不同的 return 语句中对不同范围的变量进行 return 编辑,编译器将很难执行复制省略。
如果有一种方法可以保证复制省略或在无法执行复制省略时获得某种警告,这将让开发人员确保执行复制省略并重构代码,那就太好了无法执行。