C++17:显式转换函数 vs 显式构造函数 + 隐式转换——规则变了吗?
C++17: explicit conversion function vs explicit constructor + implicit conversions - have the rules changed?
Clang 6、clang 7 和 gcc 7.1、7.2 和 7.3 都同意以下是有效的 C++17 代码,但在 C++14 和 C++11 下是不明确的。 MSVC 2015 和 2017 也接受它。但是,gcc-8.1 和 8.2 即使在 c++17 模式下也拒绝它:
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()
。
拒绝它的编译器同意之间存在歧义:
- 显式转换函数Bar::operator int()
- 首先使用从
Bar<char>
到 char
的隐式用户定义转换,然后是从 char
到 int
的隐式内置转换,然后是显式构造函数 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.
这里有几股力量在起作用。要了解发生了什么,让我们检查一下 (Foo)x
应该将我们引向何方。首先,在这种特殊情况下,c 风格的转换等同于 static_cast
。静态转换的语义是直接初始化结果对象。由于结果对象是 class 类型,[dcl.init]/17.6.2 告诉我们它的初始化如下:
Otherwise, if the initialization is direct-initialization, or if it is
copy-initialization where the cv-unqualified version of the source
type is the same class as, or a derived class of, the class of the
destination, constructors are considered. The applicable constructors
are enumerated ([over.match.ctor]), and the best one is chosen through
overload resolution. The constructor so selected is called to
initialize the object, with the initializer expression or
expression-list as its argument(s). If no constructor applies, or the
overload resolution is ambiguous, the initialization is ill-formed.
所以重载决议选择 Foo
的构造函数来调用。如果重载解析失败,则程序格式错误。在这种情况下,它不应该失败,即使我们有 3 个候选构造函数。它们是 Foo(int)
、Foo(Foo const&)
和 Foo(Foo&&)
。
首先,我们需要复制初始化一个int
作为构造函数的参数,这意味着找到一个从Bar<char>
到int
的隐式转换序列。由于您提供的从 Bar<char>
到 char
的用户定义转换运算符不是显式的,我们可以使用它来从隐式对话序列 Bar<char> -> char -> int
.
对于另外两个构造函数,我们需要将引用绑定到Foo
。但是,我们不能那样做。根据 [over.match.ref]/1 :
Under the conditions specified in [dcl.init.ref], a reference can be
bound directly to a glvalue or class prvalue that is the result of
applying a conversion function to an initializer expression. Overload
resolution is used to select the conversion function to be invoked.
Assuming that “cv1 T” is the underlying type of the reference being
initialized, and “cv S” is the type of the initializer expression,
with S a class type, the candidate functions are selected as follows:
- The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S
and yield type “lvalue reference to cv2 T2” (when initializing an
lvalue reference or an rvalue reference to function) or “ cv2 T2” or
“rvalue reference to cv2 T2” (when initializing an rvalue reference or
an lvalue reference to function), where “cv1 T” is
reference-compatible ([dcl.init.ref]) with “cv2 T2”, are candidate
functions. For direct-initialization, those explicit conversion
functions that are not hidden within S and yield type “lvalue
reference to cv2 T2” or “cv2 T2” or “rvalue reference to cv2 T2,”
respectively, where T2 is the same type as T or can be converted to
type T with a qualification conversion ([conv.qual]), are also
candidate functions.
唯一可以为我们生成 Foo
类型的 glvalue 或 prvalue 的转换函数是您指定的显式转换函数模板的特化。但是,因为函数参数的初始化不是直接初始化,我们不能考虑显式转换函数。所以我们不能在重载决议中调用复制或移动构造函数。这让我们只剩下采用 int
的构造函数。所以重载决议是成功的,应该是这样。
那为什么有些编译器会发现它有歧义,或者调用模板转换运算符呢?好吧,由于在标准中引入了有保证的复制省略,因此注意到 (CWG issue 2327) 用户定义的转换函数也应该有助于复制省略。今天,根据标准的干信,他们没有。但我们真的希望他们这样做。虽然具体应该如何完成的措辞仍在制定中,但似乎一些编译器已经开始并尝试实现它。
这就是您看到的实现。在这里干扰过载解析的是扩展复制省略的相反力量。
Clang 6、clang 7 和 gcc 7.1、7.2 和 7.3 都同意以下是有效的 C++17 代码,但在 C++14 和 C++11 下是不明确的。 MSVC 2015 和 2017 也接受它。但是,gcc-8.1 和 8.2 即使在 c++17 模式下也拒绝它:
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()
。
拒绝它的编译器同意之间存在歧义:
- 显式转换函数Bar::operator int()
- 首先使用从
Bar<char>
到char
的隐式用户定义转换,然后是从char
到int
的隐式内置转换,然后是显式构造函数 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.
这里有几股力量在起作用。要了解发生了什么,让我们检查一下 (Foo)x
应该将我们引向何方。首先,在这种特殊情况下,c 风格的转换等同于 static_cast
。静态转换的语义是直接初始化结果对象。由于结果对象是 class 类型,[dcl.init]/17.6.2 告诉我们它的初始化如下:
Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one is chosen through overload resolution. The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.
所以重载决议选择 Foo
的构造函数来调用。如果重载解析失败,则程序格式错误。在这种情况下,它不应该失败,即使我们有 3 个候选构造函数。它们是 Foo(int)
、Foo(Foo const&)
和 Foo(Foo&&)
。
首先,我们需要复制初始化一个int
作为构造函数的参数,这意味着找到一个从Bar<char>
到int
的隐式转换序列。由于您提供的从 Bar<char>
到 char
的用户定义转换运算符不是显式的,我们可以使用它来从隐式对话序列 Bar<char> -> char -> int
.
对于另外两个构造函数,我们需要将引用绑定到Foo
。但是,我们不能那样做。根据 [over.match.ref]/1 :
Under the conditions specified in [dcl.init.ref], a reference can be bound directly to a glvalue or class prvalue that is the result of applying a conversion function to an initializer expression. Overload resolution is used to select the conversion function to be invoked. Assuming that “cv1 T” is the underlying type of the reference being initialized, and “cv S” is the type of the initializer expression, with S a class type, the candidate functions are selected as follows:
- The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” (when initializing an lvalue reference or an rvalue reference to function) or “ cv2 T2” or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where “cv1 T” is reference-compatible ([dcl.init.ref]) with “cv2 T2”, are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” or “cv2 T2” or “rvalue reference to cv2 T2,” respectively, where T2 is the same type as T or can be converted to type T with a qualification conversion ([conv.qual]), are also candidate functions.
唯一可以为我们生成 Foo
类型的 glvalue 或 prvalue 的转换函数是您指定的显式转换函数模板的特化。但是,因为函数参数的初始化不是直接初始化,我们不能考虑显式转换函数。所以我们不能在重载决议中调用复制或移动构造函数。这让我们只剩下采用 int
的构造函数。所以重载决议是成功的,应该是这样。
那为什么有些编译器会发现它有歧义,或者调用模板转换运算符呢?好吧,由于在标准中引入了有保证的复制省略,因此注意到 (CWG issue 2327) 用户定义的转换函数也应该有助于复制省略。今天,根据标准的干信,他们没有。但我们真的希望他们这样做。虽然具体应该如何完成的措辞仍在制定中,但似乎一些编译器已经开始并尝试实现它。
这就是您看到的实现。在这里干扰过载解析的是扩展复制省略的相反力量。