6

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

#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 编译

4

1 回答 1

4

虽然看起来是这样,

static_cast<A<float>>(a);

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

A<float> temp_obj(A);

wheretemp_obj是已创建临时文件的发明名称。

作为结果,

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

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

A<float> b(a);

上面的形式是直接初始化

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

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

A(const A&);

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>)。

于 2021-12-22T18:43:30.497 回答