C++17:显式转换函数 vs 显式构造函数 + 隐式转换 - 规则是否改变?

15

Clang 6、clang 7和gcc 7.1、7.2和7.3都认为以下是有效的C++17代码,但在C++14和C++11下具有歧义。MSVC 2015和2017也接受它。然而,即使在c++17模式下,gcc-8.1和8.2也会拒绝它。

struct Foo
{
    explicit Foo(int ptr);
};

template<class T>
struct Bar
{
    operator T() const;
    template<typename T2>
    explicit operator T2() const;
};


Foo foo(Bar<char> x)
{
    return (Foo)x;
}

接受它的编译器选择模板化显式转换函数Bar::operator T2()。拒绝它的编译器同意存在以下二义性:1. 显式转换函数Bar::operator int();2. 首先使用从Bar<char>char的隐式用户定义转换,然后使用从charint的隐式内置转换,最后使用显式构造函数Foo(int)。那么,哪个编译器是正确的?C++14和C++17标准之间的相关差异是什么?附录:实际错误消息。在gcc-8.2 -std=c++17中的错误如下。gcc-7.2 -std=c++14输出相同的错误:
<source>: In function 'Foo foo(Bar<char>)':    
<source>:17:17: error: call of overloaded 'Foo(Bar<char>&)' is ambiguous    
     return (Foo)x;    
                 ^    
<source>:3:14: note: candidate: 'Foo::Foo(int)'    
     explicit Foo(int ptr);    
              ^~~    
<source>:1:8: note: candidate: 'constexpr Foo::Foo(const Foo&)'    
 struct Foo    
        ^~~    
<source>:1:8: note: candidate: 'constexpr Foo::Foo(Foo&&)'

以下是来自 clang-7 -std=c++14 的错误信息(clang-7 -std=c++17 可以接受此代码):

<source>:17:12: error: ambiguous conversion for C-style cast from 'Bar<char>' to 'Foo'    
    return (Foo)x;    
           ^~~~~~    
<source>:1:8: note: candidate constructor (the implicit move constructor)    
struct Foo    
       ^    
<source>:1:8: note: candidate constructor (the implicit copy constructor)    
<source>:3:14: note: candidate constructor    
    explicit Foo(int ptr);    
             ^    
1 error generated.

clang++ 6.0.1 给我一个链接器错误。命令: clang++ -fsanitize=undefined -std=c++17 -Wall -Wextra -Wshadow -Weffc++ -pedantic -pedantic-errors -Wc++14-compat -Wc++17-compat -o explicit explicit.cpp输出: /tmp/explicit-71c2bd.o: 在函数 ‘foo(Bar<char>)’ 中: explicit.cpp:(.text+0x15): 对 ‘Bar<char>::operator Foo<Foo>() const’ 未定义的引用 clang-6.0: 错误:链接器命令失败,退出代码为 1 (使用 -v 以查看调用) - Ted Lyngmo
2
@ted 自然而然。我已经简化了示例,不包括任何函数的实现,因为这在这里是无关紧要的。如果您有兴趣,可以使用-c编译,或者使用-S查看汇编输出。 - wolfgang
如果您正在使用正确的C++转换,会发生什么? - Matthieu Brucher
static_cast or function-style casts show the same behavviour, as does declaring a new variable and initializing it: Foo y(x); Foo z{x}; - wolfgang
1个回答

10
这里有几个因素在起作用。为了理解正在发生的事情,让我们看看 (Foo)x 应该引导我们到哪里。首先,这种 C 风格的转换在本例中等同于 static_cast。而静态转换的语义是直接初始化结果对象。由于结果对象将是一个类类型,[dcl.init]/17.6.2 告诉我们它的初始化如下:

否则,如果初始化是直接初始化,或者如果它是复制初始化,其中源类型的 cv 限定版本与目标类相同或是其派生类,则考虑构造函数。适用的构造函数被枚举([over.match.ctor]),并通过重载分辨选择最佳的构造函数。所选的构造函数被调用以初始化对象,并以初始化表达式或表达式列表作为其参数。如果没有构造函数适用,或者重载分辨无法确定,则初始化是非法的。

所以进行重载分辨以选择要调用的 Foo 构造函数。如果重载分辨失败,程序就是非法的。在这种情况下,即使我们有三个候选构造函数,也不应该失败。那些是 Foo(int)Foo(Foo const&)Foo(Foo&&)

首先,我们需要使用一个 int 类型的参数进行构造函数的复制初始化,这意味着需要从 Bar<char>int 找到一个隐式转换序列。由于您提供的从 Bar<char>char 的自定义转换运算符不是显式的,因此我们可以使用它来形成一个隐式转换序列 Bar<char> -> char -> int

对于另外两个构造函数,我们需要将引用绑定到 Foo 上。但我们不能这样做。根据 [over.match.ref]/1 :

在[dcl.init.ref]指定的条件下,引用可以直接绑定到glvalue或class prvalue,该prvalue是将转换函数应用于初始化表达式的结果。使用重载分辨率来选择要调用的转换函数。假设“cv1 T”是正在初始化的引用的基础类型,“cv S”是初始化表达式的类型,其中S是一个类类型,则候选函数如下所示: 考虑S及其基类的转换函数。那些非显式转换函数,不在S内隐藏,并产生类型“对cv2 T2的左值引用”(当初始化左值引用或对函数的右值引用时)或“cv2 T2”或“对cv2 T2的右值引用”(当初始化右值引用或对函数的左值引用时),其中“cv1 T”与“cv2 T2”兼容([dcl.init.ref]),是候选函数。对于直接初始化,那些显式转换函数,不在S内隐藏,并分别产生类型“对cv2 T2的左值引用”或“cv2 T2”或“对cv2 T2的右值引用”,其中T2与T相同或可以通过限定转换([conv.qual])转换为类型T,也是候选函数。
唯一能够产生类型为Foo的glvalue或prvalue的转换函数是您指定的显式转换函数模板的特化版本。但是,由于函数参数的初始化不是直接初始化,因此我们无法考虑显式转换函数。因此,我们不能在重载决议中调用复制或移动构造函数。这只留下了一个以int为参数的构造函数。因此,重载决议成功,就是这样。
那么为什么有些编译器会发现它是模棱两可的,或者调用模板转换运算符呢?好吧,自从标准引入了保证复制省略之后,就注意到(CWG issue 2327)用户定义的转换函数也应该对复制省略做出贡献。今天,根据标准的干字,他们没有。但我们真的希望他们这样做。虽然如何实现它的确切措辞仍在开发中,但似乎有些编译器已经开始尝试实现它。
而这就是你看到的实现。这是扩展复制省略的对立力量,干扰了这里的重载决议。

1
编译器生成的汇编代码检查表明,没有标记二义性的编译器都调用了显式转换函数,而不是构造函数+隐式转换链。 - wolfgang
1
@wolfgang - 是的,他们尝试实现2327。然后他们改变了实现方式。你想要一个语言律师的答案,是吗? - StoryTeller - Unslander Monica
确实,那正是我所要求的。我需要一段时间来完全理解,但这些肯定是标准的相关部分,而且它们确实已经改变了,尽管不是我预期的方向 :-) - wolfgang

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