转换和移动 ctor 导致对 Clang 和 GCC 4.9.2 的调用不明确

Cla*_*ius 6 c++ ambiguous language-lawyer c++11

我对 C++11 中的以下转换问题有点困惑。鉴于此代码:

\n\n
#include <utility>\n\nstruct State {\n  State(State const& state) = default;\n  State(State&& state) = default;\n  State() = default;\n  int x;\n};\n\ntemplate<typename T>\nstruct Wrapper {\n  T x;\n  Wrapper() = default;\n  operator T const&() const& { return x; }\n  // version which also works with GCC 4.9.2:\n  // operator T&&() && { return std::move(x); }\n  // version which does not work with GCC 4.9.2:\n  operator T() && { return std::move(x); }\n};\n\nint main() {\n  Wrapper<State> x;\n  State y(std::move(x));\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

godbolt 链接到使用 Clang 进行的失败编译

\n\n

在上面的形式中,g++ 从版本 5.1 开始以及 ICPC 版本 16 和 17 编译代码。如果我取消注释T&&转换运算符并在当前使用的第二个运算符中注释:

\n\n
  operator T&&() && { return std::move(x); }\n  // version which does not work with GCC 4.9.2:\n  // operator T() && { return std::move(x); }\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后 GCC 4.9 也可以编译。否则,它会抱怨:

\n\n
foo.cpp:23:23: error: call of overloaded \xe2\x80\x98State(std::remove_reference<Wrapper<State>&>::type)\xe2\x80\x99 is ambiguous\n   State y(std::move(x));\n                       ^\nfoo.cpp:23:23: note: candidates are:\nfoo.cpp:5:3: note: constexpr State::State(State&&)\n   State(State&& state) = default;\n   ^\nfoo.cpp:4:3: note: constexpr State::State(const State&)\n   State(State const& state) = default;\n
Run Code Online (Sandbox Code Playgroud)\n\n

然而,clang 永远不会编译代码,同样抱怨对 的构造函数的不明确调用State

\n\n

这个,我不明白。鉴于std::move(x),我希望有一个类型为 的右值Wrapper<State>。那么,\xe2\x80\x99t 转换运算符T&&() &&是否应该明显优于该运算符呢T const&() const&?鉴于此,\xe2\x80\x99t 的右值引用构造函数是否应该State用于y从转换的右值引用返回值进行构造?

\n\n

有人可以向我解释一下这种歧义吗?理想情况下,Clang 还是 GCC(如果是的话,在哪个版本中)是否正确,以及实现从包装器移入状态对象的最佳方法是什么?

\n

Bri*_*ian 2

值得注意的是,最新版本的 Clang 和 GCC 不再相互不一致。在 C++11 模式下,重载解析不明确。神箭链接

有两种初始化方式State:复制构造函数和移动构造函数。

为了调用复制构造函数,编译器需要找到从std::move(x)(xvalue of type Wrapper<State>) 到 的隐式转换序列const State&。从类类型初始化引用时,返回兼容引用类型的该类的转换函数优先于返回引用可以绑定到的临时值的转换函数。请参阅 C++11 [dcl.init.ref]/5(强调我的):

对类型“ cv1 ”的引用由类型“ cv2T1 ”的表达式初始化,如下所示: T2

  • 如果引用是左值引用并且初始化表达式

    • 是左值(但不是位字段),并且“ cv1 ”与“ cv2T1 ”引用兼容,或者 T2
    • 具有类类型(即,T2是类类型),其中T1与 不引用相关T2并且可以隐式转换为“ cv3 T3 ”类型的左值,其中“ cv1 ”与“ cv3T1引用兼容 T3(此转换通过枚举适用的转换函数(13.3.1.6)并通过重载解析选择最佳的函数(13.3)来选择,

    那么该引用在第一种情况下绑定到初始值设定项表达式左值,在第二种情况下绑定到转换的左值结果(或者在任何一种情况下,绑定到该对象的相应基类子对象)。[...]

  • 否则, [...]

因此,复制构造函数的隐式转换顺序是调用operator T const&并将State const&参数绑定到该调用的结果。

对于移动构造函数,唯一的可能性是调用operator T.

那么问题就变成这两个隐式转换序列中哪一个更好。调用operator T const&然后绑定State const&到该调用的结果?或者调用operator T然后绑定State&&到该调用的结果?

比较两个用户定义的转换序列的规则取自([over.ics.rank]/3):

如果用户定义的转换序列U1包含相同U2的用户定义转换函数或构造函数或聚合初始化,并且 的第二标准转换序列优于 的第二标准转换序列,则用户定义的转换序列U1是比另一个用户定义的转换序列更好的转换序列U2

然而,在这种情况下,涉及两个不同的用户定义的转换函数(operator T const&operator T),因此两个用户定义的转换序列是不可比较的。重载解析确实是不明确的,就像 Clang 16 和 GCC 12.2 所说的那样(在 C++11 模式下)。

请注意,Clang 和 GCC 也同意,当您operator T&&使用operator T. 在这种情况下,在确定移动构造函数的隐式转换顺序时StateState&&参数只能绑定到调用的结果operator T&&。这仍然是与参数使用的转换函数不同的转换函数State const&,因此涉及的两个隐式转换序列仍然无法比较。

当您进入 C++17 模式时,事情会变得有趣。在这种情况下,最新版本的 Clang 和 GCC 都更愿意调用operator T。这是因为它们具有实际上不在标准中的特殊重载解析规则。有关此行为的说明,请参阅P2828R0。然而,如果 P2828R0 中提出的方向被接受,那么这段代码仍然是不明确的(也许 Clang 和 GCC 必须改变它们的行为),所以我建议不要依赖它。

我想您可能真正想要的是,当const &对象表达式可以绑定到右值引用时,合格的转换函数将永远不会被选择。我不知道在当前的 C++ 中有什么方法可以做到这一点,但您可以在 C++23 中使用显式对象参数来做到这一点:

template <typename Self>
operator T const& (this Self&& self)
requires (!std::convertible_to<Self&&, Wrapper&&>) {
    return self.x;
}
Run Code Online (Sandbox Code Playgroud)