保证复制省略是如何工作的?

120
在2016年奥卢ISO C++标准会议上,一个名为通过简化值类别实现保证复制省略的提案被标准委员会投票纳入C++17标准。
保证复制省略如何工作?它是否涵盖了已经允许复制省略的一些情况,还是需要代码更改才能保证复制省略?
2个回答

173

复制省略在许多情况下是被允许的。但即使被允许,代码仍然必须能够像没有省略复制一样工作。也就是说,必须有一个可访问的复制和/或移动构造函数。

保证复制省略重新定义了许多C++概念,以便某些情况下可以省略复制/移动,实际上根本不会引发复制/移动。编译器不是省略了一个复制;标准规定根本不会发生任何复制。

考虑这个函数:

T Func() {return T();}

根据非保证拷贝省略规则,这将创建一个临时对象,然后从该临时对象移动到函数的返回值中。该移动操作有可能被省略,但即使未被使用,T 仍必须具有可访问的移动构造函数。

类似地:

T t = Func();

这是t的复制初始化。它将使用Func的返回值来进行t的复制初始化。然而,即使不会调用移动构造函数,T仍然必须具有移动构造函数。
保证的复制省略重新定义了prvalue表达式的含义。在C++17之前,prvalues是临时对象。在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的prvalue。
当然,这也可以工作:
new auto(FactoryFunction());

如果你不喜欢写类型名称。


需要注意的是,上述保证仅适用于prvalues。也就是说,在返回已命名变量时,您没有任何保证:

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

在这种情况下,t仍然必须具有可访问的复制/移动构造函数。是的,编译器可以选择优化掉复制/移动操作。但编译器仍然必须验证可访问的复制/移动构造函数的存在。
因此,对于命名返回值优化(NRVO),没有任何变化。

1
@BenVoigt:无论是否可用elision,将非平凡可复制的用户定义类型放入寄存器中都不是ABI可以执行的可行操作。 - Nicol Bolas
1
现在规则已经公开,更新一下“prvalues是初始化”的概念可能是值得的。 - Johannes Schaub - litb
8
只有对C++标准的细节了解过多,才会认为这个术语有歧义。对于99%的C++社区,我们都知道“保证的复制省略”是什么意思。实际提出该功能的论文甚至以“保证的复制省略”为标题。仅通过“简化值类别”来实现只会让用户感到困惑和难以理解。此外,这是一个错误的用词,因为这些规则并没有真正“简化”关于值类别的规则。无论你是否喜欢,“保证的复制省略”这个术语指的就是这个特性,而不包括其他任何内容。 - Nicol Bolas
1
我非常希望能够拾取一个prvalue并携带它。我猜这只是一个(一次性)std::function<T()> - Yakk - Adam Nevraumont
2
@Icebone1000:不,两个问题都不是。一旦它有了一个名称,比如参数的名称,它就不再是prvalue了。而且保证的省略仅适用于 prvalue。 - Nicol Bolas
显示剩余20条评论

2
我认为拷贝省略的细节在这里已经被很好地分享了。然而,我发现了这篇文章: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的影响。
然而,返回局部变量的命名返回值优化大多数情况下会发生,但并不保证。在一个带有不同返回语句的函数中,如果每个返回语句都返回本地作用域的变量或相同作用域的变量,则会发生。否则,如果在不同的返回语句中返回不同作用域的变量,编译器将难以执行复制省略。
如果有一种方法可以保证复制省略或获取某种警告,当无法执行复制省略时,这将很好,这将使开发人员确保执行复制省略并重新设计代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接