为何转换会涉及两个用户自定义转换函数/构造函数?标准规则求证
首先得明确直接初始化和复制初始化的核心差异,这是理解编译器行为的关键。咱们先拿一个具体例子来说明场景:
struct Y {}; struct X { operator Y() const { return Y{}; } // 用户自定义转换函数:X→Y }; struct Z { Z(Y) {} // 用户自定义构造函数:Y→Z }; // 直接初始化:编译通过 Z z_direct(X{}); // 复制初始化:编译失败 Z z_copy = X{};
你看到的差异,根源完全在C++标准对两种初始化方式的规则定义上。
直接初始化的规则依据
直接初始化的行为在标准[dcl.init]/17.6.3(对应C++17及后续版本的条款)中有明确规定:当执行直接初始化时,编译器会将源表达式作为实参,匹配目标类型的所有构造函数(包括explicit的)。
在上面的直接初始化例子里:
- 目标类型
Z的构造函数需要一个Y类型的参数; - 编译器会查找从
X到Y的转换序列——这里X的转换函数operator Y()正好满足,这是第一个用户自定义转换; - 接着用转换得到的
Y对象调用Z的构造函数完成初始化,这是第二个用户自定义的构造步骤。
这整个过程是被标准允许的,因为直接初始化的逻辑是“找能接受源表达式(或其转换结果)的构造函数”,而构造函数参数的转换序列本身允许一个用户自定义转换——两个步骤分别对应“参数转换”和“构造初始化”,不属于同一个隐式转换序列,所以不受“单个隐式序列最多一个用户自定义转换”的限制。
复制初始化的规则依据
而你提到的[dcl.init]/17.6.3(复制初始化的条款)明确了:复制初始化的核心是尝试将源表达式直接转换为目标类型,这时候只能使用一个用户自定义转换序列——要么是源通过转换函数直接转成目标类型,要么是目标类的非explicit构造函数接受源类型作为参数。
回到复制初始化的例子,X到Z需要先转成Y(第一个用户自定义转换),再转成Z(第二个),这属于两个连续的用户自定义转换,违反了隐式转换序列的限制(标准[over.best.ics]/4明确规定,一个隐式转换序列最多包含一个用户自定义转换),所以编译失败。
总结一下
直接初始化允许“源→构造函数参数(用户自定义转换)+ 参数→目标(构造函数)”的两步用户自定义操作,因为这是构造函数调用的参数转换+构造的组合,不属于单一隐式转换序列;而复制初始化要求源直接转成目标,只能有一个用户自定义转换步骤,这就是两者行为差异的标准依据。
内容的提问来源于stack exchange,提问作者Oliv

