我正在努力理解C++中的隐式转换规则,以及为什么以下简化情况下的两个隐式转换不同:
// A templated struct.
template <typename T>
struct A {};
// A templated struct that can be constructed from anything that can be
// converted to A<T>. In reality the reason the constructor is templated
// (rather than just accepting A<T>) is because there are other constructors
// that should be preferred when both would be acceptable.
template <typename T>
struct B {
template <typename U,
typename = std::enable_if_t<std::is_convertible_v<U, A<T>>>>
B(U&& u);
};
// A struct that cna be implicitly converted to A<T> or B<T>.
struct C {
template <typename T>
operator A<T> ();
template <typename T>
operator B<T> ();
};
// Another struct that can be implicitly converted to A or B, but this time only
// a specific instantiation of those templates.
struct D {
operator A<int> ();
operator B<int> ();
};
// A function that attempts to implicitly convert from both C and D to B<int>.
void Foo() {
B<int> b_from_c = C{};
B<int> b_from_d = D{};
}
当我用clang编译它时,它抱怨b_from_c
初始化不明确:
foo.cc:45:10: error: conversion from 'C' to 'B<int>' is ambiguous
B<int> b_from_c = C{};
^ ~~~
foo.cc:24:3: note: candidate constructor [with U = C, $1 = void]
B(U&& u);
^
foo.cc:33:3: note: candidate function [with T = int]
operator B<T> ();
^
这对我来说完全有意义:从C
到B<int>
有两条转换路径。
但令我困惑的是,为什么clang没有抱怨b_from_d
初始化。两者之间唯一的区别是,有问题的使用了模板化的转换函数,而被接受的则没有。我认为这与隐式转换规则或重载选择规则中的排名有关,但我不能完全把它放在一起,如果有什么不同的话,我会期望b_from_d
被拒绝,b_from_c
被接受。
有人能帮我理解吗,最好是引用标准,为什么其中一个是模棱两可的,另一个不是?
引用的文本来自C++20标准,但链接到N4861。
[dcl.init.general]/17.6.3解释了如何进行这样的复制初始化:
否则(即,对于剩余的副本初始化情况),可以从源类型转换为目标类型,或(当使用转换函数时)转换为其派生类如12.4.2.5所述进行列举,并通过过载解决方案(12.4)。如果转换无法完成或不明确,则初始化为不正规。调用所选函数时,将初始值设定项表达式作为其参数;如果函数是构造函数,调用是目标类型的cv不合格版本的prvalue其结果对象由构造函数初始化。根据根据上述规则,复制初始化的目标对象。
根据12.4.2.5(N4861中的12.4.1.4)
[…]假设cv1CCD_ 7";是正在初始化的对象的类型,
T
是类类型,候选函数选择如下:
T
的转换构造函数(11.4.8.2)是候选函数- 当初始化器表达式的类型是类类型"时cvCCD_ 10";,考虑了CCD_ 11及其基类的非显式转换函数。[…]未隐藏在
S
内并产生cv不合格版本与T
类型相同或是其派生类的类型的函数是候选函数。对转换函数的调用返回"0";参考CCD_ 14";是类型X的glvalue,因此这种转换函数被认为产生用于选择候选函数的过程的X
在这两种情况下,参数列表都有一个参数,即初始值设定项表达式。
第一个项目符号表示构造函数B::B<D>(D&&)
是一个候选函数,我们将其称为F2
。第二个项目符号表示D::operator B<int>()
是一个候选函数,我们称之为F1
。
确定F1
或F2
中的一个是否比另一个更好的第一步是确定所涉及的隐式转换序列(N4861中的[over.match.best.general]/2([over.match.best]/2))。要调用F1
,隐式转换序列是从D{}
到D::operator B<int>
的隐式对象参数(类型为D&
)。这是一个身份转换([over.ics.ref]/1)。要调用F2
,隐式转换序列是从D{}
到构造函数的参数类型D&&
。出于同样的原因,这也是一种身份转换。
通常,将D
右值绑定到右值引用将被认为是比将其绑定到左值引用更好的隐式转换序列;[over.ics.rank]/3.2.3。但是,如果两个绑定中的任何一个绑定到未使用ref限定符声明的函数的隐式对象参数(如D::operator B<int>()
),则该规则不适用。结果是,这两个隐式转换序列中的任何一个都不如另一个。
然后,我们必须在[over.match.best.general]/2(N4861中的[over.match.best]/2)中通过决胜局规则。我们可以看到,项目符号2.2不适用于我们的案例,因为我们的上下文是[over.match.copy],而不是[over.match.conv]或[over.maatch.ref]。同样,项目符号2.3不适用。这意味着2.4控制:F1
不是函数模板专用化,F2
是。过载解析成功,选择F1
。
在b_from_c
的情况下,没有任何平局决胜规则适用,过载解决方案也不明确。
我不能100%确定,但我认为这是最佳可行函数的第4点:
F1被确定为比F2更好的函数
4。或者,如果不是这样,F1是非模板函数,而F2是模板专用
在C
的情况下,C
中的转换函数和转换构造函数B
都是模板专用化,并且同样可行。
在D
的情况下,D
中的转换函数是非模板函数,因此比转换构造函数B
更好。