C++23聚合初始化计数异常及结构化绑定编译器差异咨询
聚合初始化与结构化绑定的技术问题解答
问题1:聚合成员统计函数对继承类的计数异常
问题描述
实现了如下统计聚合类型成员数量的函数,在C++23标准下,对简单聚合类A统计时输出预期的3,但对继承自A的聚合类B统计时输出2,而B实际可以用4个初始化器完成初始化。
统计函数代码:
namespace detail { struct filler_t { template<class T> constexpr operator T() { return {}; } }; template<class T> concept aggregate_type = std::is_aggregate_v<T>; } //------------------------------------------------------------------------- // return member count of an aggregate template<detail::aggregate_type T> constexpr auto aggregate_member_count(auto&& ...filler) { if constexpr (requires{ T{ filler... }; }) return aggregate_member_count<T>(detail::filler_t{}, filler...); else return sizeof...(filler) - 1; }
测试代码:
struct A { int a; unsigned b; double c; } sa{1,2,3.}; std::cout << aggregate_member_count<A>() << "\n"; // 3 - ok struct B : A { int s; } sb{0,1,2.,3}; std::cout << aggregate_member_count<B>() << "\n"; // 2?
原因分析
问题出在filler_t的隐式转换特性与C++聚合初始化规则的交互上:
- C++20及以后支持平坦扩展聚合初始化(如
B{0,1,2.,3},前3个初始化器对应基类A的成员,最后1个对应B的成员),但这种初始化方式仅当第一个初始化器无法隐式转换为基类A时才会触发。 - 统计函数中的
filler_t可以隐式转换为任意类型(包括基类A),因此B{filler_t{}, ...}会被编译器解析为:用第一个filler_t初始化基类A(单个初始化器即可完成A的部分初始化,剩余成员自动值初始化),剩余filler_t初始化B的直接成员s。 - 当传递3个
filler_t时,B{filler_t{}, filler_t{}, filler_t{}}会因B只有1个直接成员而非法,requires表达式返回false,函数返回3-1=2。但实际测试代码使用的数值无法隐式转换为A,触发了平坦初始化,因此可以用4个初始化器。
问题2:结构化绑定的编译器行为差异
问题描述
对类B进行结构化绑定时,MSVC编译通过,但GCC/Clang报错(Clang错误信息:error: cannot decompose class type 'B': both it and its base class 'A' have non-static data members),需要确认哪个编译器的行为符合标准。
测试代码:
auto [a,b,c] = sa; // ok std::cout << a << ',' << b << ',' << c << "\n"; auto [x1,x2,x3,x4] = sb; // ok - MSVC, error - clang/gcc std::cout << x1 << ',' << x2 << ',' << x3 << ',' << x4 << "\n";
标准合规性分析
GCC和Clang的行为符合C标准,MSVC的实现属于非标准扩展:
根据C标准,结构化绑定对聚合类型的分解要求:聚合类型的所有非静态数据成员必须属于最派生类,即不能存在包含非静态数据成员的基类。
类B继承自包含非静态数据成员的A,不符合结构化绑定对聚合类型的分解条件,因此编译器应当报错。MSVC允许该写法是其提供的扩展特性,并不符合标准要求。
内容的提问来源于stack exchange,提问作者Gene
相关产品推荐
相关产品推荐

