对于用户定义的转换序列;转换构造函数和转换运算符之间似乎没有优先级,它们都是候选者;
§13.3.3.1.2/1 用户定义的转换序列
用户定义的转换序列由初始标准转换序列和用户定义的转换 (12.3) 和第二个标准转换序列组成。如果用户定义的转换由构造函数 (12.3.1) 指定,则初始标准转换序列会将源类型转换为构造函数参数所需的类型。如果用户定义的转换由转换函数(12.3.2)指定,则初始标准转换序列将源类型转换为转换函数的隐式对象参数。
因此如果转换是;
B b2 = a; // ambiguous?
可能不明确,编译失败。 Clang 编译失败,g++ 接受代码并使用构造函数; demo code,VS 也接受代码。 VS 和 g++ 调用转换构造函数(根据 OP 代码)。
考虑到贴出的代码,需要考虑用户定义的转换序列(通过构造函数和转换运算符)和static_cast的使用。
§5.2.9/4 静态投射
如果声明 T t(e); 格式正确,则表达式 e 可以使用 static_cast<T>(e) 形式的 static_cast 显式转换为类型 T,对于某些发明的临时变量 t (8.5)。这种显式转换的效果与执行声明和初始化然后使用临时变量作为转换结果的效果相同。当且仅当初始化将其用作左值时,表达式e 才用作左值。
根据上面的引用,static_cast 等同于B temp(a);,因此使用了直接初始化序列。
§13.3.1.3/1 构造函数初始化
当类类型的对象被直接初始化 (8.5)、从相同或派生类类型的表达式复制初始化 (8.5) 或默认初始化 (8.5) 时,重载决议选择构造函数。对于直接初始化或默认初始化,候选函数是被初始化对象类的所有构造函数。对于复制初始化,候选函数是该类的所有转换构造函数(12.3.1)。参数列表是初始化器的表达式列表或赋值表达式。
一般情况下(不包括标记为explicit 和const 的任何构造函数和运算符),鉴于B(const A& a); 构造函数和从A 构造B,构造函数应该获胜,因为它提供考虑best viable function 时的完全匹配;因为不需要进一步的implicit conversions(第 13.3 节;重载解决方案)。
如果构造函数B(const A& a); 被删除,转换(使用static_cast<> 仍然会成功,因为用户定义的转换运算符是一个候选并且它的使用是不模糊的。
§13.3.1.4/1 通过用户定义的转换复制初始化类
在 8.5 中指定的条件下,作为类类型对象的复制初始化的一部分,可以调用用户定义的转换来将初始化表达式转换为正在初始化的对象的类型。
引用来自 C++ 标准的 N4567 草案。
在对象的构造之外调用用户定义的转换序列也很有指导意义,即调用方法。
鉴于代码清单(和上面的规则);
#include <iostream>
using namespace std;
struct A;
struct B {
B() {}
B(const A&) { cout << "called B's conversion constructor" << endl; }
};
struct A {
A() {}
operator B() const { cout << "called A's conversion operator" << endl; return B(); }
};
void func(B) {}
int main() {
A a;
B b1 = static_cast<B>(a); // 1. cast
B b2 = a; // 2. copy initialise
B b3 ( a ); // 3. direct initialise
func(a); // 4. user defined conversion
}
Clang、g++ (demo) 和 VS 提供不同的结果,因此可能有不同的合规水平。
- clang 失败 2. 和 4.
- g++ 接受 1. 到 4.
- VS 失败 4.
根据上述规则,1. 到 3. 都应该成功,因为 B 转换构造函数是候选的,不需要进一步的用户转换;直接构造和复制初始化用于这些形式。阅读标准(上面的摘录,特别是 §13.3.3.1.2/1 和 §13.3.1.4/1,然后是 §8.5/17.6.2),2. 和 4. 可能/应该失败并且模棱两可 -因为正在考虑转换构造函数和转换运算符,没有明确的顺序。
我相信这很可能是一个意想不到的用例(类型能够以这种方式相互转换;有一个论点认为哪里会有一个转换序列是一般用例)。