保证副本省略如何运作?

时间:2016-06-26 21:23:59

标签: c++ c++17 copy-elision

在2016年奥卢ISO C ++标准会议上,标准委员会将一项名为Guaranteed copy elision through simplified value categories的提案投票给C ++ 17。

保证副本省略如何运作?是否涵盖了某些已经允许复制省略的情况,或者是否需要更改代码以保证复制省略?

2 个答案:

答案 0 :(得分:105)

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

保证副本省略重新定义了许多C ++概念,以便可以省略复制/移动的某些情况实际上不会引起复制/移动 。编译器没有复制副本;该标准表示不会发生这种复制。

考虑这个功能:

T Func() {return T();}

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

类似地:

T t = Func();

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

保证副本省略redefines the meaning of a prvalue expression。前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中,以及通常的临时生命周期扩展内容。 / p>

保证elision允许你做的一件事是返回不动的对象。例如,lock_guard无法复制或移动,因此您无法使用按值返回的函数。但是有了保证的复制权,你可以。

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

new T(FactoryFunction());

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

因此,按值返回的工厂函数可以直接初始化堆分配的内存,甚至不知道它。当然,只要这些函数内部遵循保证副本省略的规则。他们必须返回T类型的prvalue。

当然,这也有效:

new auto(FactoryFunction());

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

重要的是要认识到上述保证仅适用于prvalues。也就是说,在返回命名的变量时,您无法保证:

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

在这种情况下,t必须仍具有可访问的复制/移动构造函数。是的,编译器可以选择优化复制/移动。但编译器仍必须验证是否存在可访问的复制/移动构造函数。

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

答案 1 :(得分:1)

我认为这里已经很好地分享了复制省略的细节。但是,我找到了这篇文章: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 语句中临时对象的复制省略。

然而,返回局部变量的命名返回值优化主要发生但不能保证。在具有不同 return 语句的函数中,我看到如果每个 return 语句返回局部范围的变量,或者相同范围的变量,它就会发生。否则,如果在不同的 return 语句中返回不同作用域的变量,编译器将很难进行复制省略。

如果有一种方法可以保证复制省略或在无法执行复制省略时获得某种警告,这将很好,这将使开发人员确保执行复制省略并在无法执行时重构代码'不执行。