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

Ric*_*ian 6 c++ explicit-constructor explicit-conversion

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

\n
#include <utility>\n\ntemplate <typename T = void>\nstruct First\n{\n    template <typename... Targs>\n    First(Targs&&... args) {}\n};\n\ntemplate <>\nstruct First<void> {};\n\ntemplate <typename T>\nstruct Second\n{\n    template <typename... Targs>\n    Second(Targs&&... args) {}\n};\n\ntemplate <typename... T> class A;\n\ntemplate <typename SecondType>\nclass A<SecondType>\n{\n  public:\n    A(const A&) = default;\n    explicit A(const First<void>& first) {}\n    explicit A(const Second<SecondType>& second) {}\n};\n\ntemplate <typename FirstType, typename SecondType>\nclass A<FirstType, SecondType>\n{\n  public:\n    A(const First<FirstType> & first) {}\n    explicit operator A<SecondType>() const { return A<SecondType>(First<>()); }\n};\n\nint main() {\n    A<int, float> a{First<int>(123)};\n    A<float> b = static_cast<A<float>>(a);\n\n    // test.cpp:41:41: error: call of overloaded \xe2\x80\x98A(A<int, float>&)\xe2\x80\x99 is ambiguous\n    //    41 |     A<float> b = static_cast<A<float>>(a);\n    //       |                                         ^\n    // test.cpp:28:14: note: candidate: \xe2\x80\x98A<SecondType>::A(const Second<SecondType>&) [with SecondType = float]\xe2\x80\x99\n    //    28 |     explicit A(const Second<SecondType>& second) {}\n    //       |              ^\n    // test.cpp:26:5: note: candidate: \xe2\x80\x98constexpr A<SecondType>::A(const A<SecondType>&) [with SecondType = float]\xe2\x80\x99\n    //    26 |     A(const A&) = default;\n    //       |     ^\n    \n    return 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n

use*_*522 4

虽然看起来确实如此,

static_cast<A<float>>(a);
Run Code Online (Sandbox Code Playgroud)

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

A<float> temp_obj(A);
Run Code Online (Sandbox Code Playgroud)

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

作为结果,

A<float> b = static_cast<A<float>>(a);
Run Code Online (Sandbox Code Playgroud)

除了可能有额外的移动操作之外,与

A<float> b(a);
Run Code Online (Sandbox Code Playgroud)

上面的形式是直接初始化

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

explicit A(const Second<SecondType>& second);
Run Code Online (Sandbox Code Playgroud)

A(const A&);
Run Code Online (Sandbox Code Playgroud)

explicit构造函数上的 不适用于直接初始化。)

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

在这一点上似乎不应该考虑用户定义的转换函数,因为它是显式的,并且函数参数的初始化是复制初始化,不允许显式构造函数和转换函数,但有一个特定的例外由于CWG 问题 899的解决,对于复制/移动构造函数来说,这是这样的。

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

没有任何explicit标记与此相关。Second<SecondType>只有使用as的可变参数构造函数explicit才能解决歧义。


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

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

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

在我看来,编译器已经为直接初始化实现了这种额外的省略行为,并且以这种方式,它使得省略的复制/移动构造函数候选在重载解析中比需要用户定义的普通构造函数更好的匹配转换序列。

这消除了歧义,并且仅调用用户定义的转换函数A<int, float>(使用省略的复制/移动构造函数A<float>)。