上篇讲解了 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 的确是当前最简单的方式,借助可变参数模板可以省去参数的计数问题,而只使用迭代的方式,则要构造很多索引,代码的结构亦不清晰。这是机制的问题,迟早也会有新的特性替换掉这种方式。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.