C++构造函数匹配为何不检查explicit性回退选择候选构造函数?
问题复现代码
class Y { public: Y(int) { cout << "Y\n"; } }; class X { public: X(int, const Y&) { cout << "int, const Y&\n"; } explicit X(int) { cout << "X\n"; } X(int, X&&) { cout << "int, X&&\n"; } X(const X&) { cout << "copy\n"; } X(int, int, const Y&) { cout << "int, int, const Y&\n"; } explicit X(int, int, int) { cout << "3 ints\n"; } }; int main() { X x1(1, 2); // 正常编译 X x2({ 1,2,3 }); // 编译错误,若将X(int,int,int)替换为X(int,int,X&&)则可通过编译 }
对X x1(1,2)的编译逻辑判断是准确的:当X(int)带explicit修饰时,X(int, X&&)无法成为可行候选——隐式转换语境下不允许调用explicit构造函数,因此X(int, const Y&)是唯一可行匹配,编译正常。如果移除X(int)的explicit修饰,X(int, X&&)会进入候选列表。
问题1:为什么重载决议不会因explicit属性回退匹配次优构造函数
按照C++标准规定,重载决议与explicit合法性检查是完全独立的两个流程:
- 第一步:枚举所有可行候选,仅校验实参是否能通过合法转换序列匹配形参,完全不考虑构造函数的explicit属性
- 第二步:对所有可行候选按照转换序列优先级排序,选出唯一的最优候选。其中精确匹配的优先级永远高于需要用户自定义转换的匹配
- 第三步:选出最优候选后,才校验当前初始化语境的合法性:如果是拷贝初始化、拷贝列表初始化语境,选中的构造函数被explicit修饰,直接判定程序非法,触发编译错误
整个流程不存在最优候选校验失败就回退选择次优候选的设计。
对X x2({1,2,3})这个拷贝列表初始化场景:X(int,int,int)的三个int形参与列表内三个int实参是完全匹配,转换优先级远高于其他需要用户自定义转换的候选(比如X(int,int,const Y&)需要把第三个int转换为Y临时对象,属于用户定义转换,优先级比精确匹配低一级),因此必然被选为最优候选。后续检查到该构造函数带explicit修饰,不符合拷贝列表初始化的调用要求,直接报错,不会再考虑次优选项。
如果将X(int,int,int)替换为X(int,int,X&&),该候选的第三个参数需要将int转换为X临时对象,属于用户定义转换,和X(int,int,const Y&)的转换优先级相当甚至更低,不会被优先选中,因此不会触发explicit相关的错误。
问题2:移除X(int)的explicit后GCC/Clang报歧义、MSVC不报的原因
当移除X(int)的explicit修饰后,X x1(1,2)调用会出现两个优先级完全相等的可行候选:
- 候选1:
X(int, const Y&):第一个int形参精确匹配,第二个int实参经用户定义转换构造Y临时对象,绑定到const左值引用 - 候选2:
X(int, X&&):第一个int形参精确匹配,第二个int实参经用户定义转换构造X临时对象,绑定到右值引用
按照C++标准的重载排序规则,这两个转换序列的等级完全相同,不存在谁更优,属于歧义调用,GCC和Clang的报错是严格符合标准的行为。
MSVC默认不报歧义,是因为其默认编译配置开启了大量面向旧代码的非标准兼容扩展,没有严格执行标准的重载决议规则。只要给MSVC开启/permissive-严格标准符合模式,它就会和GCC/Clang一致报出歧义错误。
内容的提问来源于stack exchange,提问作者CPPL

