Mic*_*ens 6 c++ optional overload-resolution
我试图了解类型特征传播背后的机制,如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;
}
Run Code Online (Sandbox Code Playgroud)
奖金问题
GCC 做正确的事吗?我稍微修改了这个例子以更近一步:https : //godbolt.org/z/br1vx1。在这里,我通过将它们声明为私有来使复制操作不可访问。带有 -std=c++20 的 GCC-10.2 现在使静态断言失败并抱怨
error: use of deleted function 'std::optional<NonMoveable>::optional(std::optional<NonMoveable>&&)'
Run Code Online (Sandbox Code Playgroud)
根据为什么 C++11-deleted 函数参与重载决议?在重载决议之后应用删除,这可能表明移动构造函数参与,尽管 P0602R4 表示不应。
另一方面https://en.cppreference.com/w/cpp/language/overload_resolution一开始就说明
... 如果这些步骤产生多个候选函数,则执行重载决议 ...
所以跳过重载决议,因为移动构造函数是唯一的候选者?
复制操作的处理有细微的差别,复制操作应有条件地定义为删除,而移动操作不应参与重载决策。
其背后的要求(以及如何实现这些要求)std::optional非常复杂。但是,移动操作不应参与重载决策(对于不可移动类型)与删除复制操作(对于不可复制类型)的要求可能与单独的主题相关;
我们可以通过查看比 更简单的类型来理解这个主题std::optional。
(1) 命名返回值优化
C++ 中正在扩展的移动急切性(C++20 中的移动急切性更高)意味着在某些特殊情况下,即使移动构造函数已被删除,也会选择移动构造函数而不是复制构造函数。对于不可移动类型来说,避免这些问题的唯一方法是通过了解 5 规则以及控制这些是否隐式定义的规则,确保该类型没有移动构造函数或移动赋值运算符。
对于复制而言,不存在相同的偏好,并且如果可能的话,没有理由倾向于删除它们而不是删除它们(2)。换句话说,在重载决策中存在着偏爱移动的相同问题,有时会意外地选择移动而不是复制,但反之则不存在;复制移动。
(2) 不存在复制构造函数和复制赋值运算符(尽管它们可能被定义为删除),就不存在类这样的东西。
考虑以下类型:
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;
};
Run Code Online (Sandbox Code Playgroud)
在哪里,A:
而且B,其中 :
在我们继续使用不同的标准版本之前,我们定义了以下我们将返回的函数:
A getA() {
A a{};
return a;
}
B getB() {
B b{};
return b;
}
Run Code Online (Sandbox Code Playgroud)
现在,在 C++14 中,允许在某些情况下实现复制(/移动)省略;引用N4140 中的[class.copy]/31(C++14 + 编辑修复)[强调我的]:
当满足某些条件时,允许实现省略类对象的复制/移动构造,即使为复制/移动操作选择的构造函数和/或对象的析构函数具有副作用。[...]
这种复制/移动操作的省略称为复制省略,在以下情况下是允许的:
- 在具有类返回类型的函数的 return 语句中,当表达式是非易失性自动对象(函数或 catch 子句参数除外)的名称时,该对象具有与函数返回类型相同的 cv 非限定类型,通过将自动对象直接构造到函数的返回值中,可以省略复制/移动操作
- [...]
并且,来自[class.copy]/32 [强调我的]:
当满足复制/移动操作的省略条件,但不满足异常声明,并且要复制的对象由左值指定时,或者当 return 语句中的表达式是(可能带括号的)id-命名具有自动存储持续时间的对象的表达式,该对象在最内层封闭函数或 lambda 表达式的主体或参数声明子句中声明,首先执行重载解析以选择副本的构造函数,就像该对象由右值指定一样。
但是[class.temporary]/1仍然对对象的被删除副本施加相同的语义限制,就好像副本实际上没有被删除一样 [强调我的]
[..] 即使临时对象的创建未被评估(子句 [expr])或以其他方式避免([class.copy]),也应遵守所有语义限制,就好像临时对象已被创建并随后被销毁一样。
这样,即使在复制省略符合条件(并执行)的情况下,来自(例如,适用于 NRVO 的命名对象)的转换序列也需要通过重载解析来查找(可能被省略)转换构造函数,并开始使用传递重载决策,就好像该对象是由右值指定的一样。这意味着,在 C++14 中,以下内容是格式良好的
auto aa{getA()}; // OK, and copy most likely elided.
Run Code Online (Sandbox Code Playgroud)
而以下格式不正确:
auto bb{getB()}; // error: use of deleted function 'B::B(B&&)'
Run Code Online (Sandbox Code Playgroud)
因为重载解析会在将 in视为右值B的步骤中找到已声明但已删除的移动构造函数。对于,不存在移动构造函数,这意味着in in with作为右值的重载解析将失败,并且此后没有此扭结的重载解析将成功找到 的复制构造函数(随后将被忽略)。breturn b;getB()Aareturn a;getA()aA
现在,在 C++17 中,通过延迟(最终完全省略)临时物化的概念,复制省略变得更加强大,特别是添加[class.temporary]/3 [重点是我的]:
当类类型 X 的对象传递给函数或从函数返回时,如果 X 的每个复制构造函数、移动构造函数和析构函数都是平凡的或已删除,并且 X 至少有一个未删除的复制或移动构造函数,则实现为允许创建临时对象来保存函数参数或结果对象。临时对象分别从函数参数或返回值构造,并且函数的参数或返回对象被初始化,就像使用未删除的普通构造函数来复制临时对象一样(即使该构造函数不可访问或不会被选择)通过重载解析来执行对象的复制或移动)。
这产生了很大的差异,因为现在可以执行复制省略,而getB()无需通过返回值重载解析的特殊规则(之前选择了删除的移动构造函数),因此这两者在 C++17 中都是格式良好的:
auto aa(getA()); // OK, copy elided.
auto bb(getB()); // OK, copy elided.
Run Code Online (Sandbox Code Playgroud)
C++20 实现了P1825R0,它允许更多隐式移动,扩展了可能发生移动构造或赋值的情况,即使乍一看期望复制构造/赋值(可能被省略)。
关于移动渴望(过度复制)的相当复杂的规则可能会产生一些意想不到的效果,并且如果设计者想要确保类型不会遇到已删除的移动构造函数或移动赋值运算符在重载解析中优先的极端情况对于未删除的复制构造函数或复制赋值运算符,最好确保没有可用于重载解析的移动构造函数/赋值运算符(对于这些情况),而不是声明它们并将它们显式定义为 -已删除。但是,此参数不适用于移动向量/复制赋值运算符,如下所示:
作为一个例子(可能是 GCC 回归错误),说明对于非语言律师而言,获得这些规则是困难的,GCC trunk 目前拒绝 C++20 的以下程序(DEMO):
// B as above
B getB() {
B b{};
return b;
}
Run Code Online (Sandbox Code Playgroud)
与错误消息
Run Code Online (Sandbox Code Playgroud)error: use of deleted function 'B::B(B&&)'
在这种情况下,人们会期望在上面选择一个副本(可能被省略),以防B删除其移动向量。如有疑问,请确保移动构造函数和赋值运算符不参与(即存在)重载决策。
(3) 可以声明一个用const- 和 ref 限定符重载的删除赋值运算符,例如,这在重载解决方案(分配给const A& operator=(const A&) const && = delete;右值)期间很少是可行的候选者,并且这将保证其他非-const 和-qualified 重载,否则可能是有效的重载候选者。 const&
| 归档时间: |
|
| 查看次数: |
280 次 |
| 最近记录: |