T231119 几种遍历技术实现结构体成员计数的比较
上篇讲解了 Compile time for,它是遍历技术中的迭代方式之一,特别常用。
它一般与 Fold expressions 组合使用,它们两个,一个相当于编译期的 Range-based for,一个相当于编译期的 for loop。由于 std::index_sequence<N>
本身只是一个静态序列,并不具备向前迭代的能力,而 Fold expressions 天生就是干这个的,两相组合,Compile time for 的功能便可完全具备。
再者,Fold expressions 只适合用来局部访问数据,不支持全局操作,当你想要整体性操作某一数据集合时,除了递归,这种迭代方式是唯一选择。相较起来,其他方式都要更加麻烦,参考上篇的例子。
考虑之前群内讨论过的一个例子,求结构体成员个数,来对比一下几种常见的编译期遍历技术。
下面是一种递归解法:
struct Any { template <class T> operator T() const; };
template <class T, class... Args>
requires std::is_aggregate_v<T>
consteval auto CountAggregateMembers() {
if constexpr (requires { T{ Args{}... }; }) {
return CountAggregateMembers<T, Args..., Any>();
} else {
return sizeof...(Args) - 1;
}
}
struct S {
int a;
double b;
std::string c;
};
int main() {
// 3
std::cout << CountAggregateMembers<S>();
}
C++17 constexpr if
极大地简化了这种实现,避免编写多个递归函数。
在此之前,递归类解法是比较麻烦的。
比如,使用 Tag dispatching 来实现相同功能:
template <bool v>
struct type_tag { enum { value = v }; };
template <class T, class... Args>
std::size_t CountAggregateMembersImpl(type_tag<true>) {
return CountAggregateMembersImpl<T, Args..., Any>(
type_tag<std::is_aggregate_v<T> && std::is_constructible_v<T, Args..., Any>>{}
);
}
template <class T, class... Args>
std::size_t CountAggregateMembersImpl(type_tag<false>) {
return sizeof...(Args) - 1;
}
template <class T>
std::size_t CountAggregateMembers() {
return CountAggregateMembersImpl<T>(type_tag<true>{});
}
这种方式的一个缺点是,在面对非聚合类型时,无法强制产生编译期错误,而是会返回 0。
使用 SFINAE 来实现:
template <class T, class... Args>
std::enable_if_t<std::is_aggregate_v<T> && !std::is_constructible_v<T, Args...>, std::size_t>
CountAggregateMembers() {
return sizeof...(Args) - 1;
}
template <class T, class... Args>
std::enable_if_t<std::is_aggregate_v<T> && std::is_constructible_v<T, Args...>, std::size_t>
CountAggregateMembers() {
return CountAggregateMembers<T, Args..., Any>();
}
几乎都不可避免地要多写几个函数,所以能够看出 constexpr if
是一个跨越式的进步。
那怎么使用迭代的方法来实现相同的功能呢?下面是一种做法:
template <class T, std::size_t N>
concept ConstructibleWithN = requires {
[]<std::size_t... I>(std::index_sequence<I...>) -> decltype(T{ (I, Any{})... }) {
return {};
}(std::make_index_sequence<N>{});
};
template <class T, std::size_t N>
concept CanAggregate = std::is_aggregate_v<T> && ConstructibleWithN<T, N> && !ConstructibleWithN<T, N + 1>;
template <class T>
constexpr auto CountAggregateMembers = []<std::size_t... I>(std::index_sequence<I...>) {
std::size_t R;
((CanAggregate<T, I> ? (R = I, true) : false) || ...);
return R;
}(std::make_index_sequence<64>{});
这种做法采取两层上节介绍的 Compile time for,外层用于产生循环的结束条件,这里选择了一个相对较大的数字;内层用于实际测试当前个数的聚合构建是否能够成功,借助高级 Fold expressions 的技巧,只要找到便会立即停止迭代。
在实现该功能上,constexpr if
的确是当前最简单的方式,借助可变参数模板可以省去参数的计数问题,而只使用迭代的方式,则要构造很多索引,代码的结构亦不清晰。这是机制的问题,迟早也会有新的特性替换掉这种方式。