【问题标题】:Is copy/move elision allowed to make a program using deleted functions well-formed?复制/移动省略是否允许使用格式良好的已删除函数制作程序?
【发布时间】:2017-09-06 09:47:39
【问题描述】:

考虑以下代码:

#include <iostream>

struct Thing
{
    Thing(void)                       {std::cout << __PRETTY_FUNCTION__ << std::endl;}
    Thing(Thing const &)              = delete;
    Thing(Thing &&)                   = delete;
    Thing & operator =(Thing const &) = delete;
    Thing & operator =(Thing &&)      = delete;
};

int main()
{
    Thing thing{Thing{}};
}

我希望Thing thing{Thing{}}; 语句意味着使用默认构造函数构造Thing 类的临时对象,以及使用刚刚创建的临时对象作为参数的移动构造函数构造Thing 类的thing 对象。而且我希望这个程序被认为是格式错误的,因为它包含对已删除移动构造函数的调用,即使它可能被省略。标准的 class.copy.elision 部分似乎也要求这样做:

即使调用被省略,所选的构造函数也必须是可访问的

Wording for guaranteed copy elision through simplified value categories 似乎也不允许这样做。

但是 gcc 7.2(以及 clang 4,但不是 VS2017 still does not support guaranteed copy elision)将 compile this code just fine 省略移动构造函数调用。

在这种情况下哪种行为是正确的?

【问题讨论】:

  • 你为什么希望这里有一个临时的,而不是在 Thing thing{f()}; 那里 f() 返回一个 Thing
  • 另外,我怀疑“可访问”意味着“非私有”。
  • @BoPersson 不,它也适用于私有构造函数。
  • @BoPersson 我希望Thing thing{f()}; 表示调用移动构造函数,并使用f() 返回的临时参数调用一个参数。希望避免创建临时的开销。
  • 通过简化值类别保证复制省略的措辞似乎也不允许这样做确实如此,但令人惊讶的是很少有人理解如何(或他们不急于回答)。所选答案仅显示示例,但它不是规范性措辞。并且示例前面的规范措辞并不能保证整个 T(T(T())) 折叠成 () 或者重载决议不会选择复制/移动构造函数。

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


【解决方案1】:

它不会生成格式错误的程序。它完全摆脱了对已删除函数的引用。提案中的适当措辞如下:

[dcl.init] 项目符号 17.6

如果初始化表达式是纯右值并且 cv 不合格 源类型的版本与 目的地,初始化表达式用于初始化 目标对象。 [ 示例:T x = T(T(T()));调用 T 默认值 构造函数来初始化 x。 ]

这个例子进一步强化了这一点。因为它表明整个表达式必须折叠成一个默认构造。

需要注意的是,由于值类别而省略副本时,删除的函数永远不会被使用,因此程序引用它。

这是一个重要的区别,因为复制省略的 other 形式仍然使用复制 c'tor,如下所述:

[basic.def.odr]/3

... 选择用于复制或移动类类型对象的构造函数是 odr-used 即使调用实际上被实现忽略了 ([class.copy] ...

[class.copy] 描述了另一种形式的允许(但不是强制性)复制省略。哪一个,如果我们在你的课堂上演示:

Thing foo() {
    Thing t;
    return t; // Can be elided according to [class.copy.elision] still odr-used
}

应该使程序格式错误。 And GCC complains about it as expected.


顺便说一句。如果您认为在线编译器中的前面示例是魔术师的把戏,而 GCC 会抱怨,因为它需要 调用 move c'tor。 Have a look at what happens when we supply a definition.

【讨论】:

  • 感谢您的详细回复。我认为下一个(17.6.2)项目符号实际上更有趣:“否则,如果初始化是直接初始化,或者是复制初始化,其中源类型的 cv 非限定版本与相同的类,或者一个派生类,目的地的类,构造函数被考虑。”那是在传递适当的prvalue时不考虑右值引用构造函数,所以程序似乎保持良好的格式,因为没有考虑删除的构造函数。但这似乎与复制/移动省略部分的要求有些不一致。
  • @VTT - 复制/移动省略部分实际上是另一回事。我认为有必要注意,虽然该提案通俗地说是“强制复制/移动省略”,但它使用了“临时实现”一词。所以本质上,它没有定义可能被省略的副本。它只是将初始化延迟到尽可能晚的阶段,因此实际上不需要副本。魔法就是这样发生的。
  • 问题在于,如果不考虑这种延迟初始化的已删除/不可访问构造函数,则可以pass objects around by value ultimately leading to object construction in places that would be otherwise prohibited to do so。我可以想象一些好处(例如封装mutex 的类,并有一个方法直接返回lock_guard,而不是返回对mutex 的引用以在用户端构造lock_guard)。但感觉还是不对。
  • @VTT - 我明白为什么感觉不对。这是一种重大的范式转变。但正如你所说,它的好处也一样多。
  • @VTT:你没有传递对象。您正在传递纯右值,它们明确还不是对象。 That's the trick that makes guaranteed elision work. A prvalue is not an object yet. It is an initializer for an object. 通过重新定义纯右值的含义,您可以删除本应被省略的操作。
猜你喜欢
  • 1970-01-01
  • 2014-01-02
  • 1970-01-01
  • 2023-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-26
相关资源
最近更新 更多