0%

simulate constexpr for in C++

這是一種常見的code pattern

1
2
3
4
5
6
template <size_t length>
void for_loop_do() {
for (size_t i = 0; i < length; i++) {
do(i);
}
}

不過既然我們在compile-time知道length的值了,自然想要unroll loop body,變成類似這樣

1
2
3
4
5
6
void unroll_do() {
do(0);
do(1);
do(2);
....
}

直覺的想法就是costexpr for

1
2
3
4
5
6
template <size_t length>
void for_loop_do() {
constexpr for (size_t i = 0; i < length; i++) {
do(i);
}
}

不過自然沒這語法,只好用其他方法繞過,解決的方法也不只一種,提出兩種比較常用的

Version 1: Recursive + if constexpr

學過資料結構的話,知道迴圈可以用遞洄來模擬
加上C++17的if constexpr可以很容易蹶決這問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <size_t index, size_t boundary, typename Func>

void constexpr_for_impl(Func func)
{
    if constexpr (index == boundary) {
        return;
    } else {
        func(index);
        constexpr_for_impl<index + 1, boundary>(func);
    }
}

template <size_t boundary, typename Func>
void constexpr_for(Func func)
{
    constexpr_for_impl<0, boundary>(func);
}

int main()
{
    constexpr_for<3>([](size_t index) {
        printf("llu\n", index);
    });
}

Version 2: integer_sequence

C++14之後,引進了一個helper class integer_sequence,將常數納入Type當中
這很少直接拿來用,通常只有操作tuple時才會使用,不過要模擬constexpr for這個特色,這功能少不了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <utility>

template <typename T, T... ints>
void print_sequence(std::integer_sequence<T, ints...> int_seq)
{
    std::cout << "The sequence of size " << int_seq.size() << ": ";
    ((std::cout << ints << ' '), ...);
    std::cout << '\n';
}

int main()
{
    print_sequence(std::integer_sequence<unsigned, 9, 2, 5, 1, 9, 1, 6>{});
}

我們想要的就是有一個類似std::integer_sequence<size_t, 0, 1, ..., n>這樣的sequence,這時候另外一個helper type make_integer_sequence登場了
以下是上一個範例的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <utility>

template <typename T, T... ints>
void print_sequence(std::integer_sequence<T, ints...> int_seq)
{
    std::cout << "The sequence of size " << int_seq.size() << ": ";
    ((std::cout << ints << ' '), ...);
    std::cout << '\n';
}

int main()
{
    print_sequence(std::make_integer_sequence<int, 12>{});
}

這就是constexpr for的核心觀念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <std::size_t... Is, typename Func>
void constexpr_for_impl(std::index_sequence<Is...>, Func func)
{
    (func(Is), ...);
}

template <size_t boundary, typename Func>
void constexpr_for(Func func)
{
    constexpr_for_impl(std::make_index_sequence<boundary>{}, func);
}

int main()
{
    constexpr_for<3>([](size_t index) {
        printf("llu\n", index);
    });
}

C++20之後,有了 Template Lambdas,因此我們可以進一步這樣寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <size_t boundary, typename Func>
void constexpr_for(Func func)
{
    [&] <size_t... Is> (std::index_sequence<Is...>) {
        ([&] <size_t I> (std::integral_constant<size_t, I>) {
            func(I);
        } (std::integral_constant<size_t, Is>{}), ...);
    } (std::make_index_sequence<boundary>{});
}

int main()
{
    constexpr_for<3>([](size_t index) {
        printf("llu\n", index);
    });
}

Conclusion

模擬只是模擬,當for loop加入了continuebreakreturn之後,上面的方法就會變得很複雜,只適合簡單的應用