std::可选:不参与重载解析与被定义为已删除



我试图理解类型特征传播背后的机制,如 std::optional 在 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0602r4.html 中描述的那样。复制操作(应有条件地定义为已删除)与移动操作(不应参与重载解析)的处理方式存在细微差别。

造成这种差异的原因是什么,我将如何测试后者?例:

#include <type_traits>
#include <optional>
struct NonMoveable {
NonMoveable() = default;
NonMoveable(NonMoveable const&) = default;
NonMoveable(NonMoveable&&) = delete;
NonMoveable& operator=(NonMoveable const&) = default;
NonMoveable& operator=(NonMoveable&&) = delete;
};
// Inner traits as expected
static_assert(!std::is_move_constructible<NonMoveable>::value);
static_assert(!std::is_move_assignable<NonMoveable>::value);
// The wrapper is moveable, via copy operations participating in
// overload resolution. How to verify that the move operations don't?
static_assert(std::is_move_constructible<std::optional<NonMoveable>>::value);
static_assert(std::is_move_assignable<std::optional<NonMoveable>>::value);
int main(int argc, char* argv[])
{
NonMoveable a1;
NonMoveable a2{std::move(a1)}; // Bad, as expected
std::optional<NonMoveable> b1;
std::optional<NonMoveable> b2{std::move(b1)}; // Good, see above. But
// useless as a test for
// P0602R4.
return 0;
}

奖金问题

海湾合作委员会做正确的事情吗?我对示例进行了一些修改,以更接近一小步:https://godbolt.org/z/br1vx1。在这里,我通过将复制操作声明为私有来使其无法访问。带有 -std=c++20 的 GCC-10.2 现在无法通过静态断言并抱怨

error: use of deleted function 'std::optional<NonMoveable>::optional(std::optional<NonMoveable>&&)'

根据为什么 C++11 删除的函数参与重载解析? 删除是在重载解析后应用的,这可能表明移动构造函数参与了,尽管P0602R4说它不应该参与。

另一方面,https://en.cppreference.com/w/cpp/language/overload_resolution 一开始就说

。如果这些步骤产生多个候选函数,则执行重载解析...

所以跳过了重载解析,因为移动构造函数是唯一的候选者?

std::optional 是一个红鲱鱼;关键是理解导致为什么将这些要求放在库类型的机制

复制操作(

应有条件地定义为已删除)与移动操作(不应参与重载解析)的处理方式存在细微差别。

std::optional的底层要求(以及如何实现这些要求)很复杂。但是,移动操作不应参与重载解析(对于不可移动类型)与要删除的复制操作(对于不可复制类型)的要求可能与单独的主题有关;

  • NRVO(1)(如果可以的话,可以的话,复制省略的情况)和
  • 更多隐式移动,例如,从函数返回具有自动存储持续时间的命名对象时,选择移动构造函数而不是复制构造函数。

我们可以通过查看比std::optional更简单的类型来理解这个主题。

(1) 命名返回值优化

<小时 />

TLDR

C++中正在扩展的移动渴望(C++20 中更渴望的移动)意味着存在特殊情况,即使移动构造函数已被删除,也会选择移动构造函数而不是复制构造函数。避免这些类型(例如,非移动类型)的唯一方法是确保该类型没有移动构造函数或移动赋值运算符,方法是了解 5 规则以及控制这些是否隐式定义的内容。

复制不存在同样的偏好,如果可能的话,没有理由赞成删除它们而不是删除它们(2)。换句话说,对于支持重载分辨率移动而存在的相同扭结,有时出乎意料地选择移动而不是复制,则相反;复制移动。

(2)没有复制 ctor 和复制赋值运算符(尽管它们可以定义为删除)就没有类这样的东西。


隐式移动急切

请考虑以下类型:

struct A {
A() { std::cout << __PRETTY_FUNCTION__ << "n"; }
A(A const &) { std::cout << __PRETTY_FUNCTION__ << "n"; }
A &operator=(A const &) {
std::cout << __PRETTY_FUNCTION__ << "n";
return *this;
}
};
struct B {
B() { std::cout << __PRETTY_FUNCTION__ << "n"; }
B(B const &) { std::cout << __PRETTY_FUNCTION__ << "n"; }
B &operator=(B const &) {
std::cout << __PRETTY_FUNCTION__ << "n";
return *this;
}
B(B &&) = delete;
B &operator=(B &&) = delete;
};

其中,A

  • 具有用户定义的构造函数,因此移动构造函数和移动辅助运算符不会被隐式定义,

以及B的地方,此外:

声明和
  • 删除移动构造函数和移动赋值运算符;当它们被声明定义为已删除时,它们将参与重载解析。

在我们继续浏览不同的标准版本之前,我们定义了以下函数,我们将返回这些函数:

A getA() {
A a{};
return a;
}
B getB() {
B b{};
return b;
}

C++14

现在,在 C++14 中,允许实现在某些场景中实现 copy(/move) elision;引用 N4140 中的 [class.copy]/31(C++14 + 编辑修复)[强调我的]:

当满足某些条件时,即使为复制/移动操作和/或对象的析构函数选择的构造函数具有副作用,也允许实现省略类对象的复制/移动构造。[...]

在以下情况下,允许对复制/移动操作进行这种省略(称为复制省略):

  • 在具有类返回类型的函数的 return 语句中,当表达式是与函数返回类型具有相同 CV-UNQUALIFIED 类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作
  • [...]

并且,从 [class.copy]/32 [强调我的]:

满足复制/移动操作的省略条件,但不符合异常声明的条件,并且要复制的对象由左值指定时,或者当 return 语句中的表达式是一个(可能用括号括起来的)id-表达式时,该表达式命名了一个对象,该表达式具有在最内层封闭函数或 lambda 表达式的主体或参数声明子句中声明的自动存储持续时间,首先执行为副本选择构造函数的重载解析,就像对象由右值指定一样

但是 [class.temporary]/1 仍然对对象的省略副本施加了相同的语义限制,就好像该副本实际上没有被省略一样 [强调我的]

[..]即使临时对象的创建未求值(子句 [expr])或以其他方式避免([class.copy]),也应遵守所有语义限制,就像临时对象已被创建并随后销毁一样

这样,即使对于复制 elision 符合条件(并执行)的情况,来自 NRVO 可用的命名对象的转换序列也需要通过重载解析来查找(可能省略)转换构造函数,并且将从传递重载解析开始,就好像该对象由右值指定一样。这意味着,在 C++14 中,以下内容的格式正确

auto aa{getA()};  // OK, and copy most likely elided.

而以下内容格式不正确

auto bb{getB()};  // error: use of deleted function 'B::B(B&&)'    

由于重载解析将在将return b;中的b视为getB()中的右值的步骤中找到B声明但已删除的移动构造函数。对于A,不存在移动构造函数,这意味着a的重载解析在return a;getA()中,a作为右值将失败,因此没有这个扭结的重载解析将成功地找到A的复制构造函数(随后将被省略)。

C++17

现在,在 C++17 中,由于临时的延迟(完全省略)实现的概念,特别是添加了 [class.temporary]/3 [强调我的],副本省略变得更加强大:

当类类型 X 的对象传递给函数或从函数返回时,如果 X 的每个复制构造函数、移动构造函数和析构函数都是微不足道的或删除的,并且 X 至少有一个未删除的副本或移动构造函数,则允许实现创建一个临时对象来保存函数参数或结果对象。临时对象分别由函数参数或返回值构造,函数的参数或返回对象初始化,就像使用未删除的普通构造函数复制临时构造函数一样(即使该构造函数不可访问或不会被重载解析选中以执行对象的复制或移动)。

这有很大的不同,因为现在可以对getB()执行复制 elision,而无需传递返回值重载解析的特殊规则(之前选择了已删除的移动构造函数),因此这两个规则在 C++17 中都是格式良好的:

auto aa(getA());  // OK, copy elided.
auto bb(getB());  // OK, copy elided.

C++20

C++20 实现了允许更多隐式移动的P1825R0,扩展了移动构造或分配可能发生的情况,即使乍一看期望复制构造/分配(可能省略)。

<小时 />

总结

关于移动急切(过度复制)的相当复杂的规则可能会产生一些意想不到的效果,如果设计人员想要确保类型不会遇到删除的移动构造函数或移动赋值运算符在重载解析中优先于未删除的复制构造函数或复制赋值运算符的极端情况, 与声明它们并将其定义为显式删除相比,最好确保没有可用于查找重载解析的移动 CTOR/赋值运算符(对于这些情况)。但是,此参数不适用于移动 ctor/复制赋值运算符,因为:

  • 该标准不包含类似的复制急切(过度移动),并且
  • 没有没有复制构造函数或复制赋值运算符的类这样的东西,并且基本上(3)只能在 C++20 中使用 requires-Clauses 从重载解析中删除它们。

作为为非语言律师制定这些规则的困难的一个例子(可能是GCC回归错误),GCC主干目前拒绝以下C++20(DEMO)程序:

// B as above
B getB() {
B b{};
return b;
}

带有错误消息

error: use of deleted function 'B::B(B&&)'

在这种情况下,人们会期望在上面选择一个副本(可能省略),以防B删除了它的移动 ctor。如有疑问,请确保移动 ctor 和赋值运算符不参与(即存在)重载解析。

(3)可以声明一个已删除的赋值运算符,重载了const和ref限定符,比如const A& operator=(const A&) const && = delete;,在重载解(赋值constrvalue)期间,它很少是一个可行的候选者,并且这将保证不存在其他非常量和&限定的重载,否则这些重载可能是有效的重载候选者。

最新更新