转换和移动 ctor 导致对 Clang 和 GCC 4.9.2 的模棱两可的要求

Conversion and move ctor leads to ambiguous call for Clang and GCC 4.9.2

本文关键字:模棱两可 Clang ctor 移动 转换 GCC      更新时间:2023-10-16

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

#include <utility>
struct State {
State(State const& state) = default;
State(State&& state) = default;
State() = default;
int x;
};
template<typename T>
struct Wrapper {
T x;
Wrapper() = default;
operator T const&() const& { return x; }
// version which also works with GCC 4.9.2:
// operator T&&() && { return std::move(x); }
// version which does not work with GCC 4.9.2:
operator T() && { return std::move(x); }
};
int main() {
Wrapper<State> x;
State y(std::move(x));
}

Godbolt 链接到与 Clang 的失败编译

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

operator T&&() && { return std::move(x); }
// version which does not work with GCC 4.9.2:
// operator T() && { return std::move(x); }

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

foo.cpp:23:23: error: call of overloaded ‘State(std::remove_reference<Wrapper<State>&>::type)’ is ambiguous
State y(std::move(x));
^
foo.cpp:23:23: note: candidates are:
foo.cpp:5:3: note: constexpr State::State(State&&)
State(State&& state) = default;
^
foo.cpp:4:3: note: constexpr State::State(const State&)
State(State const& state) = default;

但是,clang 从不编译代码,同样抱怨对State构造函数的模棱两可的调用。

这个,我不明白。考虑到std::move(x),我希望有一个类型Wrapper<State>的右值。那么,转换运算符T&&() &&不应该明显优于T const&() const&运算符吗?鉴于此,State的右值引用构造函数不应该用于从转换的右值引用返回值构造y吗?

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

值得注意的是,最新版本的 Clang 和 GCC 不再相互不同意。在 C++11 模式下,过载分辨率不明确。神螺栓链接

State初始化有两个候选对象:复制构造函数和移动构造函数。

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

对类型">cv1T1"的引用由类型">cv2T2"的表达式初始化,如下所示:

  • 如果引用是左值引用和初始值设定项表达式

    • 是一个左值(但不是位字段),并且">cv1T1"与">cv2T2"兼容,或
    • 具有类类型(即,T2是类类型),其中T1T2无关,并且可以隐式转换为类型为 "cv3T3" 的左值,其中 "cv1T1" 与 ">cv3T3" 引用兼容(此转换是通过枚举适用的转换函数 (13.3.1.6) 并通过重载分辨率 (13.3) 选择最佳转换函数来选择的),

    然后,在第一种情况下,引用绑定到初始值设定项表达式 lvalue,并绑定到 lvalue 结果 在第二种情况下的转换(或者,在任一情况下,转换为适当的基类子对象 对象)。[...]

  • 否则, [...]

因此,复制构造函数的隐式转换序列是调用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时,代码仍然不明确(在 C++11 模式下)。在这种情况下,在确定Statemove 构造函数的隐式转换序列时,State&&参数只能绑定到调用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;
}