使用空初始化列表时GCC未选中预期重载operator=()的问题
我想设计一个支持任意类型<T>的包装类my_class,要求支持值赋值,简化后的代码如下:
template<typename T> class my_class { private: T m_value; public: explicit my_class() = default; my_class(const my_class&) = delete; my_class& operator=(const my_class&) = delete; template<typename U = T> my_class& operator=(U&& u) { m_value = std::forward<U>(u); std::cout << "Value assigned" << std::endl; return *this; } };
该类的构造函数为explicit,且禁用了拷贝操作,我预期任何值或初始化列表都能赋值给my_class。
但执行以下代码时:
my_class<std::string> foo; foo = {};
MSVC和Clang的运行结果符合预期,会打印Value assigned;但GCC无法编译,报错如下:
<source>:25:10: error: use of deleted function 'my_class<T>& my_class<T>::operator=(const my_class<T>&) [with T = std::__cxx11::basic_string<char>]' 25 | foo={}; | ^ <source>:13:15: note: declared here 13 | my_class& operator=(const my_class&) = delete; | ^~~~~~~~
补充说明
- 使用任何非空初始化列表时代码可正常编译:
my_class<std::string> foo; foo = {'a', 'b', 'c'}; - 若不使用模板,直接定义如下重载的
operator=:
GCC会报如下错误:my_class& operator=(T t) { m_value = std::move(t); std::cout << "Value assigned" << std::endl; return *this; }<source>:30:10: error: ambiguous overload for 'operator=' (operand types are 'my_class<std::__cxx11::basic_string<char> >' and '<brace-enclosed initializer list>') 30 | foo={}; | ^ <source>:19:15: note: candidate: 'my_class<T>& my_class<T>::operator=(const my_class<T>&) [with T = std::__cxx11::basic_string<char>]' (deleted) 19 | my_class& operator=(const my_class&) = delete; | ^~~~~~~~ <source>:21:15: note: candidate: 'my_class<T>& my_class<T>::operator=(T) [with T = std::__cxx11::basic_string<char>]' 21 | my_class& operator=(T u) { |
核心问题
为什么在my_class构造函数为explicit的情况下,GCC会认为存在重载歧义,而MSVC和Clang却能正常编译?
原因解析
这本质是不同编译器对C++标准中重载解析规则的实现差异,核心在于空初始化列表{}的转换路径判断:
foo = {}的解析逻辑
这是拷贝赋值的语法形式,编译器需要将{}转换为右侧操作数,此时有两个候选重载:- 被删除的
operator=(const my_class&):要匹配这个重载,需要把{}隐式转换为my_class对象,但my_class的默认构造函数是explicit,根据C++标准,explicit构造函数不能用于隐式转换,这条路径本应被排除。 - 模板版
operator=(U&&):当U默认推导为T(这里是std::string)时,{}可以隐式触发std::string的默认构造,从而匹配这个重载,这也是符合预期的路径。
- 被删除的
GCC的异常行为
GCC在处理空初始化列表时,错误地将explicit构造函数的转换路径纳入了重载候选,即使标准明确禁止这种隐式转换。对于非空初始化列表(如{'a','b','c'}),编译器会直接将其匹配为T的初始化列表构造,不会触发my_class的转换路径,因此编译正常。非模板
operator=(T)的歧义问题
此时存在两个候选重载:被删除的拷贝赋值和operator=(T)。GCC错误地认为{}既可以尝试转换为my_class(忽略了explicit构造的限制),又可以转换为T,因此报出重载歧义;而Clang和MSVC正确排除了explicit构造的转换路径,只匹配operator=(T),因此编译正常。
简言之,这是GCC在重载解析阶段对explicit构造函数与空初始化列表的交互处理不符合C++标准的bug。
内容的提问来源于stack exchange,提问作者guard3

