std :: optional:不参与过载解析,而被定义为已删除 C ++ 14 C ++ 17 C ++ 20 摘要

问题描述

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

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

#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;
}

奖励问题

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

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

根据Why do C++11-deleted functions participate in overload resolution?,删除是在重载解析之后应用的,尽管P0602R4表示不愿意,但这可能表明move构造函数已参与。

另一方面,https://en.cppreference.com/w/cpp/language/overload_resolution声明在开头

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

因此跳过了重载解决方案,因为move构造函数是唯一的候选对象?

解决方法

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在哪里:

  • 具有用户定义的构造函数,这样就不会隐式定义move构造函数和move assigment运算符。

还有B

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

在继续使用不同的标准版本之前,我们定义以下将返回的功能:

A getA() {
    A a{};
    return a;
}

B getB() {
    B b{};
    return b;
}

C ++ 14

现在,在C ++ 14中,允许在某些情况下实现 实现copy(/ move)省略;引用N4140中的[class.copy]/31(C ++ 14 +编辑修订)[强调我的]:

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

在以下情况下,允许使用此复制/移动操作的缩写,称为 :

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

并且来自[class.copy]/32 [强调我的]:

当满足复制/移动操作的省略标准,但不满足异常声明的条件,并且要复制的对象由左值指定时,或者当return语句中的表达式为(可能带有括号时) )id-expression,该对象使用在最里面的封闭函数或lambda-expression的主体或参数声明子句中声明的自动存储持续时间命名的对象,首先执行重载分辨率以选择副本的构造函数,就像对象由右值指定

但是[class.temporary]/1仍然对删除的对象副本施加相同的语义限制,就好像实际上并未删除该副本一样[强调我的]

[..]即使对临时对象的创建未进行评估(条款[expr])或以其他方式避免([class.copy]),也应遵守所有语义限制,就像临时对象具有被创建并随后被销毁。

即使在符合复制消除条件(并已执行)的情况下,例如对于NRVO而言可行的命名对象的转换序列也需要通过重载解析来查找(可能被淘汰)转换构造函数,并以通过重载分辨率开头,就好像该对象是由右值指定的。。这意味着在C ++ 14 中,以下内容格式正确

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

以下内容格式错误

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

因为过载解析会在将Bb中的return b;中的getB()视为右值的步骤中找到A的已声明但已删除的move构造函数。对于a,没有移动构造函数 存在,这意味着return a;getA()的{​​{1}}中a的{​​{1}}的重载解析将会失败,此后没有这种纠结的重载解析将成功找到A的副本构造函数(随后将其删除)。

C ++ 17

现在,在C ++ 17中,通过临时对象的延迟(完全淘汰)实现的概念增强了复制的灵活性,尤其是添加了[class.temporary]/3 [强调我的]:>

当将类型X的对象传递给或从函数返回时,如果X的每个副本构造函数,移动构造函数和析构函数都是平凡的或已删除,则至少允许一个未删除的副本或移动构造函数,实现可以创建一个临时对象来容纳功能参数或结果对象临时对象分别由函数参数或返回值构建,,并且函数的参数或返回对象被初始化,就像通过使用未删除的琐碎构造函数来复制临时对象(即使该构造函数不可访问或不会被重载决议选择来执行对象的复制或移动)。

这有很大的不同,因为现在可以为getB()执行复制省略,而无需通过返回值重载解析的特殊规则(以前选择了删除的move构造函数),因此这两个都很好在C ++ 17中形成:

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

C ++ 20

C ++ 20实现了P1825R0,它甚至允许more implicit moves扩展了可能进行移动构造或分配的情况,即使乍一看他们希望进行复制构造/分配(可能消失)。


摘要

关于移动急切性(过度复制)的相当复杂的规则可能会产生一些意想不到的影响,并且如果设计人员要确保类型不会碰到一个极端的情况,其中删除的move构造函数或move赋值运算符将优先在未删除副本构造函数或副本分配运算符上进行重载解析时,与声明并定义它们相比,最好确保没有可用于重载解析的移动ctor /赋值运算符来查找(针对这些情况)明确删除。但是,此参数不适用于移动ctor /副本分配运算符,如下所示:

  • 该标准不包含类似的复制要求(过度),并且
  • 没有像没有复制构造函数或复制赋值运算符的类之类的东西,从重载解析中删除它们基本上是(3),只有在使用要求子句的C ++ 20中才有可能。

例如,对于难以使用非语言律师的规则很难解决的一个示例(可能还有GCC回归错误),GCC主干当前拒绝以下C ++ 20程序(DEMO):

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

显示错误消息

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

在这种情况下,如果B删除了它的移动ctor,可能会希望在上面选择一个副本(可能被删除)。如有疑问,请确保运动控制器和赋值运算符不参与(即存在)过载解析。

(3)可以声明一个已删除的赋值运算符,该赋值运算符重载const和ref限定词,例如const A& operator=(const A&) const && = delete;,在过载解决方案中,赋值运算符很少是可行的候选对象(赋值 to const rvalue),这将保证不存在其他非const和&限定的重载,否则可能是有效的重载候选。 / sub>

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...