静态转换的调用存在歧义

15

我有一些像这样的代码

struct B
{
    B() {}
    B(int v) {}
};

struct A
{
    operator int() const { return 1; }
    operator B() const { return B(); }
};

int main()
{
    A a;
    static_cast<B>(a); // Error here
    a.operator B(); // This is OK
    return 0;
}

它会产生这样的编译错误:

main.cpp: In function ‘int main()’:
main.cpp:16:21: error: call of overloaded ‘B(A&)’ is ambiguous
     static_cast<B>(a);
                     ^
main.cpp:4:5: note: candidate: B::B(int)
     B(int v) {}
     ^
main.cpp:1:8: note: candidate: constexpr B::B(const B&)
 struct B
        ^
main.cpp:1:8: note: candidate: constexpr B::B(B&&)

我不要求如何修复这个问题,只是想了解为什么编译器不接受它?在我看来,static_cast<B>(a)等同于a.operator B(),但似乎编译器读取方式不同。

更新:

这种行为发生在c++17之前。使用c++17,则此代码将不会产生任何编译错误。


3
static_cast<B>(a) 可能意味着 B(a.operator int())a.operator B()。需要将其翻译为:使用static_cast<B>(a)语句时,可能会被解释为执行B(a.operator int())a.operator B()操作。 - M.M
1
虽然在g++ 7.2中没有出现任何错误,但它选择了a.operator B()作为static_cast - M.M
2
将 (至少) operator int 设为 explicit 可以解决这个问题,并且可能是个好主意。g ++ 表示在 C++17 中可以使用此方法。 - Ry-
@M.M 没错,使用 -std=c++1z 没有错误,但是使用 -std=c++14 会出现错误。 - Ivan Romanov
4
我认为GCC 7.2接受-std=c++1z此处提供的背景有关,即GCC对于担保复制省略的看法。 - StoryTeller - Unslander Monica
2
这也发生在VS2017中。更奇怪的是,B b = static_cast<B>(a);是有歧义的,而B b = a;却没问题。 - Killzone Kid
2个回答

2

C++14

根据N3797 [expr.static.cast]第4段:

如果声明 T t(e); 对于某个虚构的临时变量 t 是良好形式,那么表达式 e 可以使用形如 static_cast<T>(e)static_cast 显式转换为类型 T。进行这种显式转换的效果与执行声明和初始化,然后使用临时变量作为转换结果相同。

表达式 static_cast<B>(a) 从初始化程序 a 中直接初始化一个类型为 B 的临时变量。 然后,根据N3797 [dcl.init]第17段中的以下项目适用:

如果初始化是直接初始化,或者如果它是复制初始化,其中源类型的cv-unqualified版本是目标类的同一类或派生类,则将考虑构造函数。适用的构造函数被枚举(13.3.1.3),并且通过重载解析(13.3)选择最佳构造函数。所选的构造函数将被调用以初始化对象,并使用初始化表达式或表达式列表作为其参数。如果没有构造函数适用,或者重载解析不明确,则初始化无效。
而“适用的构造函数”由N3797 [over.match.ctor]定义:
对于直接初始化,候选函数是正在初始化的对象的所有构造函数。
所以三个构造函数:B::B(int)B::B(const B&)B::B(B&&) 都是重载决议期间的候选项。然后比较三个对应的隐式转换序列:A->intA->const B&A->B&&。结果是,A->int 可以与其他两个区分开来,因为它不满足 N3797 [over.ics.rank] 段落 3 中以下项目的条件:

如果用户定义的转换序列 U1 包含与另一个用户定义的转换序列 U2 相同的用户定义的转换函数或构造函数,或者它们在聚合初始化中初始化相同的类,并且在任一情况下,U1 的第二个标准转换序列优于 U2 的第二个标准转换序列,则用户定义的转换序列 U1 比用户定义的转换序列 U2 更好。

此外,在 N3797 [over.match.best] 段落 1 中没有确定最佳可行函数的特殊规则适用,因此重载决议是有歧义的,这会导致编译器错误。

C++17

上述推断同样成立,因此这是一个编译器的错误(目前为止)。由于CWG 242,[expr.static.cast]第4段的措辞已经发生了变化,但它并不影响我们的结论。
请注意,保证复制省略在这里不适用,因为它不是从相同类型的 prvalue 进行初始化。
C++20 或更新版本
已经有一个discussion讨论关于将保证复制省略添加到这种直接初始化情况中,因此也许在未来选择 a.operator B() 的行为将是合法的。

简而言之,这意味着。表达式 static_cast<B>(a); 的编译器找到了两个替换:B(a.operator int())B(a.operator B())。但是 a.operator B() 不是一个替换。这正确吗? - Ivan Romanov

2

这是因为在C++17中,直接初始化的规则(特别是对于复制省略的规则)有所改变,因此static_cast<B>(a);仅仅会产生以下结果:

a.operator B();

cppreference/direct_initialization所述:

如果初始化程序是一个prvalue表达式,其类型与T相同(忽略cv-qualification),则使用初始化程序本身而不是从中实现的临时对象来初始化目标对象:参见复制省略(自C ++17以来)

进一步阅读cppreference/copy_elision时,它说自C++17以来,编译器必须省略复制和移动构造,如果

在函数调用中,如果return语句的操作数是prvalue,并且函数的返回类型与该prvalue的类型相同。

因此,虽然static_cast<B>(a);在C++17之前既可以解释为B(a.operator int())也可以解释为a.operator B(),但C++17必须选择第二个选项,因为a.operator B()的返回值是类型为Bprvalue,并且它可以省略复制/移动构造。

如果初始化表达式是一个 prvalue 表达式,其类型与 T 类相同...为什么应该应用这个子弹点而不是下一个:T 的构造函数将被检查,并通过重载解析选择最佳匹配项。然后调用构造函数来初始化对象。 - xskxzr
@xskxzr,不确定规则是否总是按优先顺序列出,但即使没有这个 - 选择不调用构造函数而不是选择最佳匹配似乎是合理的。 - nVxx
1
这些规则按优先顺序列出(http://www.eel.is/c++draft/dcl.init#17.6)。此外,您认为这种省略是合理的想法本质上是 CWG 2327(https://dev59.com/eFYM5IYBdhLWcg3wjAuw#48696012)。 - xskxzr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接