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

uqjltbpv  于 2023-04-01  发布在  其他
关注(0)|答案(1)|浏览(126)

我有点被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 link to the failed compilation with Clang
在上面的形式中,g++从版本5.1开始,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(如果是,在哪个版本中)是正确的,以及实现从 Package 器到状态对象的最佳方式是什么?

7z5jn7bk

7z5jn7bk1#

值得注意的是,Clang和GCC的最新版本不再相互矛盾。在C11模式下,重载解决方案是不明确的。Godbolt link
State的初始化有两个候选:复制构造函数和移动构造函数。
为了调用复制构造函数,编译器需要从std::move(x)中找到一个隐式转换序列(类型Wrapper<State>的x值)到const State&。当从类类型初始化引用时,该类中返回兼容引用类型的转换函数优先于返回引用可以绑定到的临时变量的转换函数。参见C
11 [dcl.init.ref]/5(强调我的):
对类型“cv1T1”的引用由类型“* cv 2 * T2”的表达式初始化,如下所示:

  • 如果引用是左值引用并且初始化器表达式
  • 是左值(但不是位字段),并且“cv1T1”与“* cv 2 * T2”是引用兼容的,或者
  • 有一个类类型(即T2是类类型),其中T1T2没有引用相关,并且可以隐式转换为类型为“ cv 3 * T3”的左值,“其中“cv1T1“与“ cv 3 * T3“兼容**(通过列举适用的转换函数(13.3.1.6)并通过过载解析(13.3)选择最佳转换函数来选择该转换),

那么在第一种情况下,该引用被绑定到初始化表达式左值,在第二种情况下,该引用被绑定到转换的左值结果(或者,在任何一种情况下,都被绑定到对象的适当基类子对象)。

  • 否则,[...]

因此,复制构造函数的隐式转换序列是调用operator T const&并将State const&参数绑定到该调用的结果。
对于move构造函数,唯一的可能性是调用operator T
那么问题就变成了这两个隐式转换序列中哪一个更好。调用operator T const&,然后将State const&绑定到该调用的结果?还是调用operator T,然后将State&&绑定到该调用的结果?
比较两个用户定义的转换序列的规则取自([over.ics.rank]/3):
如果用户定义转换序列U1包含相同的用户定义转换函数或构造函数或聚合初始化,并且第二个标准转换序列U1优于第二个标准转换序列U2,则用户定义转换序列U1优于另一个用户定义转换序列U2
然而,在这种情况下,涉及到两个不同的用户定义转换函数(operator T const&operator T),因此这两个用户定义转换序列是不可比的。重载解决方案确实是模糊的,就像Clang 16和GCC 12.2所说的那样(在C11模式下)。
并且请注意,Clang和GCC也同意代码仍然是模糊的(在C
11模式下)当你有operator T&&而不是operator T时。在这种情况下,当确定State移动构造函数的隐式转换序列时,State&&参数只能绑定到调用operator T&&的结果。这仍然是一个与State const&参数使用的转换函数不同的转换函数,因此所涉及的两个隐式转换序列仍然是不可比较的。
当你进入C17模式时,事情变得有趣起来。在这种情况下,Clang和GCC的最新版本都更喜欢调用operator T。这是因为它们有特殊的重载解析规则,而这些规则实际上并不在标准中。有关此行为的解释,请参见P2828R0。然而,如果接受P2828 R 0中提出的方向,那么这段代码仍然是不明确的(也许Clang和GCC将不得不改变它们的行为),所以我建议不要依赖它。
我想你可能真正想要的是,当对象表达式可以绑定到右值引用时,const &限定转换函数永远不会被选中。我不知道在当前C
中有什么方法可以做到这一点,但你可以在C++23中使用显式对象参数来做到这一点:

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

相关问题