在C++中禁用复制省略

Disable copy elision in C++

本文关键字:复制省 C++      更新时间:2023-10-16

免责声明:研究的目标是如何禁用复制省略和返回值优化提供的代码部分。如果想提到XY问题之类的东西,请避免回答。这个问题具有严格的技术和研究性质,并以这种方式强烈提出

在 C++14 中引入了复制省略和返回值优化。如果某个对象在一个表达式中被破坏并复制构造,例如复制赋值或逐值从函数返回即时值,则省略复制构造函数。

以下推理应用于复制构造函数,但可以对移动构造函数执行类似的推理,因此不再考虑。

有一些部分解决方案可用于禁用自定义代码的复制省略:

1) 依赖于编译器的选项。对于 GCC,有基于__attribule__#pragma GCC结构的解决方案,例如 https://stackoverflow.com/a/33475393/7878274 .但由于它依赖于编译器,因此没有遇到问题。

2)强制禁用复制构造函数,如Clazz(const Clazz&) = delete。或者将复制构造函数声明为explicit以防止它使用。这样的解决方案没有满足任务,因为它改变了复制语义并强制引入自定义名称函数,如Class::copy(const Clazz&)

3)使用中间类型,如这里描述 https://stackoverflow.com/a/16238053/7878274。由于此解决方案强制引入新的后代类型,因此没有遇到问题。

经过一些研究,发现恢复暂时价值可以解决问题。如果将源类重新解释为对此类的单元素数组的引用并提取第一个元素,则复制省略将关闭。模板函数可以这样写:

template<typename T, typename ... Args> T noelide(Args ... args) {
return (((T(&)[1])(T(args...)))[0]);
}

这种解决方案在大多数情况下效果很好。在下面的代码中,它生成三个复制构造函数调用 - 一个用于直接复制赋值,两个用于从函数返回的赋值。它在MSVC 2017中效果很好

#include <iostream>
class Clazz {
public: int q;
Clazz(int q) : q(q) { std::cout << "Default constructor " << q << std::endl; }
Clazz(const Clazz& cl) : q(cl.q) { std::cout << "Copy constructor " << q << std::endl; }
~Clazz() { std::cout << "Destructor " << q << std::endl; }
};
template<typename T, typename ... Args> T noelide(Args ... args) {
return (((T(&)[1])(T(args...)))[0]);
}
Clazz func(int q) {
return noelide<Clazz>(q);
}
int main() {
Clazz a = noelide<Clazz>(10);
Clazz b = func(20);
const Clazz& c = func(30);
return 0;
}

此方法适用于ab情况,但使用案例c执行冗余复制 - 应通过生存期扩展返回对临时的引用,而不是复制。

问题:如何修改noelide模板以允许它与具有生命周期扩展的常量左值引用正常工作? 谢谢!

根据 N4140, 12.8.31:

复制/移动操作的这种省略(称为复制消除)是 在以下情况下允许(可以合并到 消除多个副本):

(31.1) — 在具有类返回类型的函数的 return 语句中, 当表达式是非易失性自动对象的名称时 (函数或捕获子句参数除外)具有相同的 CV-非限定类型作为函数返回类型,复制/移动 通过直接构造自动对象可以省略操作 到函数的返回值中

(31.3) — 当一个临时类对象尚未绑定到 引用 (12.2) 将被复制/移动到具有相同内容的类对象 CV-不合格类型,复制/移动操作可以省略 将临时对象直接构造到目标中 省略的复制/移动

因此,如果我理解正确,则只有在 return 语句是局部变量的名称时,才会发生复制省略。因此,例如,您可以通过返回"禁用"复制省略,例如return std::move(value)......如果您不喜欢为此使用move,您可以简单地将noelide实现为static_cast<T&&>(...).

鉴于您的所有限制,这是不可能的。简单地说,因为该标准不提供关闭 RVO 优化的方法。

您可以通过违反其中一个要求来阻止强制应用 RVO,但无法可靠地阻止可选的允许优化。此时,您所做的一切都是更改语义或特定于编译器(例如-fno-elide-constructorsGCC 和 Clang 的选项)。