为什么我的显式构造函数会为我的转换运算符造成这种歧义?

Why is my explicit constructor creating this ambiguity for my conversion operator?

我无法弄清楚为什么我的转换运算符正在考虑显式构造函数。

#include <utility>

template <typename T = void>
struct First
{
    template <typename... Targs>
    First(Targs&&... args) {}
};

template <>
struct First<void> {};

template <typename T>
struct Second
{
    template <typename... Targs>
    Second(Targs&&... args) {}
};

template <typename... T> class A;

template <typename SecondType>
class A<SecondType>
{
  public:
    A(const A&) = default;
    explicit A(const First<void>& first) {}
    explicit A(const Second<SecondType>& second) {}
};

template <typename FirstType, typename SecondType>
class A<FirstType, SecondType>
{
  public:
    A(const First<FirstType> & first) {}
    explicit operator A<SecondType>() const { return A<SecondType>(First<>()); }
};

int main() {
    A<int, float> a{First<int>(123)};
    A<float> b = static_cast<A<float>>(a);

    // test.cpp:41:41: error: call of overloaded ‘A(A<int, float>&)’ is ambiguous
    //    41 |     A<float> b = static_cast<A<float>>(a);
    //       |                                         ^
    // test.cpp:28:14: note: candidate: ‘A<SecondType>::A(const Second<SecondType>&) [with SecondType = float]’
    //    28 |     explicit A(const Second<SecondType>& second) {}
    //       |              ^
    // test.cpp:26:5: note: candidate: ‘constexpr A<SecondType>::A(const A<SecondType>&) [with SecondType = float]’
    //    26 |     A(const A&) = default;
    //       |     ^
    
    return 0;
}

如果我像这样直接调用运算符:A<float> b = a.operator A<float>(); 那么它工作正常,所以我想知道是否有一些关于 static_cast<> 用于调用转换运算符的规则我不知道知道关于。但我发现很难理解的是,据我所知,当我没有以任何方式显式调用它们时,为什么它甚至会考虑显式构造函数。

我正在用 g++ 编译 (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0

虽然看起来确实如此,

static_cast<A<float>>(a);

实际上并没有首先尝试调用用户定义的转换函数。实际上它的行为与想象中的声明相同

A<float> temp_obj(A);

其中 temp_obj 是为创建的临时文件发明的名称。

因此,

A<float> b = static_cast<A<float>>(a);

除了额外的移动操作外,与

相同
A<float> b(a);

以上形式为直接初始化.

在直接初始化中,只考虑目标 class 的构造函数。 考虑参数类型的用户定义转换函数。 在您的情况下,有两个可行的候选构造函数:

explicit A(const Second<SecondType>& second);

A(const A&);

(构造函数上的explicit对直接初始化没有作用。)

这两个都是可行的,并且都需要对参数进行一个用户定义的转换。第一个参数是通过 Second<SecondType> 的可变参数构造函数获得的,第二个参数是通过 A<int, float>.

的用户定义转换函数获得的

此时似乎不应该考虑用户自定义的转换函数,因为它是显式的,并且函数参数的初始化是复制初始化,不允许显式构造函数和转换函数,但是由于 CWG issue 899.

的解析,copy/move 构造函数有一个特定的例外

这给我们留下了两个可行的构造函数,它们都具有同样好的转换序列。结果,结构不明确,编译器是正确的。

None 中的 explicit 标记与此相关。只有将 Second<SecondType> 的可变参数构造函数设为 explicit 才能解决歧义。


但是,如果您使用 --std=c++17 或更高版本,您将看到代码将在 Clang 和 GCC 中编译。

这可能是因为在 C++17 中引入了强制复制省略。在许多情况下,现在必须在通常需要调用的地方省略 copy/move 构造函数。

新规则实际上并不适用于我们上面调用的复制构造函数,但因为这可能只是标准中的一个疏忽,所以有一个开放的 CWG issue 2327 考虑复制省略是否应该适用于此直接初始化也是如此。

在我看来,编译器已经为直接初始化实现了这种额外的省略行为,并且以这种方式使省略的 copy/move 构造函数候选在重载决议中比需要用户定义的转换序列的普通构造函数。

这消除了歧义,只调用了 A<int, float> 的用户定义转换函数(省略了 A<float> 的 copy/move 构造函数)。