为啥我的显式构造函数会为我的转换运算符创建这种歧义?

Posted

技术标签:

【中文标题】为啥我的显式构造函数会为我的转换运算符创建这种歧义?【英文标题】:Why is my explicit constructor creating this ambiguity for my conversion operator?为什么我的显式构造函数会为我的转换运算符创建这种歧义? 【发布时间】:2022-01-23 19:59:31 【问题描述】:

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

#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> aFirst<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&lt;float&gt; b = a.operator A&lt;float&gt;(); 那么它工作正常,所以我想知道是否有一些关于 static_cast 用于调用我不知道的转换运算符的规则。但我发现很难理解的是,当我没有以任何方式显式调用显式构造函数时,为什么它甚至会考虑显式构造函数。

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

【问题讨论】:

看起来像是在 gcc 11 中修复的编译器错误:godbolt.org/z/zjhh41zh8 @InnocentBystander 不,那只是因为在该版本的 GCC 中默认语言标准修订版增加到 C++17。如果您使用--std=c++14,您会得到与问题相同的结果。 【参考方案1】:

虽然看起来是这样,

static_cast<A<float>>(a);

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

A<float> temp_obj(A);

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

因此,

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

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

相同
A<float> b(a);

上面的形式是直接初始化

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

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

A(const A&);

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

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

此时似乎不应该考虑用户定义的转换函数,因为它是显式的,并且函数参数的初始化是复制初始化,不允许显式构造函数和转换函数,但是有由于CWG issue 899 的解析,复制/移动构造函数的一个特定例外。

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

explicit 标记均与此无关。只有将Second&lt;SecondType&gt; 的可变参数构造函数设为explicit 才能解决歧义。


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

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

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

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

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

【讨论】:

为了确认这一点,我自己尝试了static_cast&lt;A&lt;float&gt;&gt;(a);,这也有同样的问题。所以,今天我了解到static_cast&lt;T&gt;(x) 不是调用显式转换运算符的正确方法。我很难过这种情况,因为感觉没有很好的方法可以做到这一点,我们现在必须在需要确定的地方使用x.operator T(),并且根据具体情况,处理不t 具有显式运算符。 @RichardF*** 如果考虑到相关的 CWG 问题,这种行为是无意的,我不会感到惊讶。在我看来,--std=c++17 的当前 Clang/GCC 行为更有意义,它实际上更喜欢static_cast 和其他直接初始化的转换运算符。也许在某个时候会出现违反标准的缺陷报告。

以上是关于为啥我的显式构造函数会为我的转换运算符创建这种歧义?的主要内容,如果未能解决你的问题,请参考以下文章

运算符重载中的显式构造?

我可以内联指定我的显式类型比较器吗?

真的没有来自 std::string_view 的 std::string 的显式构造函数吗?

带多个参数的显式构造函数

智能转换与 KOTLIN 中的显式转换有何不同

多参数构造函数上的显式关键字?