如何保证副本省略工作?

在2016年的 Oulu ISO C + + 标准会议上,标准委员会投票通过了一项名为 通过简化值类别保证省略副本的提案。

保证拷贝省略到底是如何工作的?是否包括已经允许删除副本的情况,或者是否需要修改代码以保证删除副本?

18912 次浏览

在许多情况下允许发生拷贝省略。然而,即使这是允许的,代码仍然必须能够工作,仿佛副本没有省略。也就是说,必须有一个可访问的拷贝和/或 move 构造函数。

保证省略复制重新定义了许多 C + + 概念,例如在某些情况下可以省略复制/移动,但实际上不会引起复制/移动 完全没有。编译器并没有省略一个副本; 标准规定不会发生这样的复制。

考虑一下这个函数:

T Func() {return T();}

在非保证复制省略规则下,这将创建一个临时值,然后从该临时值移动到函数的返回值。移动操作 被省略,但是 T必须仍然有一个可访问的移动构造函数,即使它从未被使用过。

Similarly:

T t = Func();

这是 t的副本初始化。这将使用返回值 Func复制初始化 t。但是,T仍然必须有一个 move 构造函数,即使它不会被调用。

保证拷贝省略。在 C + + 17之前,prvalue 是临时对象。在 C + + 17中,prvalue 表达式仅仅是一个能够使 实现成为临时表达式的东西,但它还不是临时表达式。

如果使用 prvalue 初始化 prvalue 类型的对象,则不会实现临时值。当执行 return T();时,这将通过 prvalue 初始化函数的返回值。由于该函数返回 T,因此不会创建临时值; prvalue 的初始化只是直接初始化返回值。

需要理解的是,由于返回值是一个 prvalue,因此它是 不是物品。它仅仅是一个对象的初始化程序,就像 T()一样。

当执行 T t = Func();时,返回值的 prvalue 直接初始化对象 t; 不存在“创建临时并复制/移动”阶段。由于 Func()的返回值是等价于 T()的 prvalue,所以 tT()直接初始化,就好像您已经初始化了 T t = T()一样。

如果以任何其他方式使用 prvalue,prvalue 将具体化一个临时对象,这个临时对象将在该表达式中使用(如果没有表达式,则丢弃该临时对象)。因此,如果使用 const T &rt = Func();,则 prvalue 将具体化一个临时值(使用 T()作为初始值设定项) ,其引用将与通常的临时生存期扩展一起存储在 rt中。

省略允许你做的一件事就是返回不可移动的对象。例如,不能复制或移动 lock_guard,因此不能有按值返回它的函数。但是有了保证的拷贝省略,你可以。

保证省略也适用于直接初始化:

new T(FactoryFunction());

如果 FactoryFunction按值返回 T,则此表达式将不会将返回值复制到分配的内存中。相反,它将分配内存并使用 分配的内存作为函数调用的返回值内存。

因此,通过值返回的工厂函数可以在不知情的情况下直接初始化堆分配的内存。当然,只要这些函数 在内部遵循保证拷贝省略的规则。它们必须返回 T类型的值。

当然,这也有效:

new auto(FactoryFunction());

如果你不喜欢写类型名的话。


重要的是要认识到,以上保证只有工作的价值。也就是说,返回 名字变量时不能保证:

T Func()
{
T t = ...;
...
return t;
}

在这个例子中,t必须仍然有一个可访问的 copy/move 构造函数。是的,编译器可以选择优化掉复制/移动。但编译器仍必须验证是否存在可访问的复制/移动构造函数。

因此,命名返回值优化(NRVO)没有任何改变。

我认为这里已经很好地分享了省略副本的细节。然而,我发现了这篇文章: https://jonasdevlieghere.com/guaranteed-copy-elision,它指的是 C + + 17中在返回值优化情况下保证省略的副本。

It also refers to how using the gcc option: -fno-elide-constructors, one can disable the copy elision and see that instead of the constructor directly being called at destination, we see 2 copy constructors(or move constructors in c++11) and their corresponding destructors being called. Following example shows both cases:

#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 语句的函数中,我发现如果每个 return 语句返回局部作用域的变量,或者返回相同作用域的变量,就会发生这种情况。否则,如果在不同的返回语句中返回不同作用域的变量,编译器将很难执行复制省略。

如果有一种方法可以保证拷贝省略,或者在拷贝省略不能执行时得到某种警告,这将使开发人员确保拷贝省略被执行,并且在不能执行时重构代码,那将是非常好的。