编译时计算

C++的一大好处就是可以在编译时进行一些非常有意思的操作。虽然频繁的编译时计算会显著的增加编译时间,但是同样的也会减小运行时负担。当然前面这个只不过是一方面的说辞,我使用模板或者constexpr这些特性纯粹是因为这样看起来比较有趣。最近我就从Bisqwit的一个视频中学到了一个将循环转到编译时的一个小技巧。故水个博客记录一下。

所有的测试代码可以在这里找到。

1
2
3
4
constexpr int iter_times = 10;
for (size_t i = 0; i < iter_times; ++i) {
do_something(i);
}

上面的这段代码是一段稀松平常的循环,我们利用for语句进行了10次循环,每次循环都去执行do_something()这个函数。首先我们先考虑怎么把这个东西用编译时循环进行展开。我们所使用的到工具叫做std::index_sequence以及std::make_index_sequence,具体的代码如下所示。

1
2
3
constexpr int iter_times = 10;
[&]<std::size_t... p>(std::index_sequence<p...>) { (do_something(p), ...); }
(std::make_index_sequence<iter_times>{});

std::make_index_sequence<iter_times>会产生{0, 1, 2, ..., iter_times - 1}的参数序列,我们再将其传入到函数参数之中,而又因为参数是可变的,所以我们自然要用可变模板参数来进行接收。在这之后,函数体内的...就会帮助我们进行展开,使其本质上转换为以下形式。

1
do_something(0) op do_something(1) op do_something(2) ...

而这里的op就是我们的逗号,所以上面的代码会转换为do_something(0), do_something(1), do_something(2), ..., 也就是说假如你的do_something()函数如果能返回一个可计算的值的话,这里的op也可以替换为*, +, /,等计算符号,使其能够进行结果计算,这种情况下其实我们利用的就是C++17的fold expression特性了。

原理就是如上所示,下面给出一段能运行的代码示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
* Compile time loop test
*/

#include <iostream>
#include <utility>

constexpr int iter_times = 10;

int main() {
constexpr auto do_something = [&](size_t i) { std::cout << i << " "; };

constexpr auto do_another_thing = [&](size_t i) { return i; };
// 普通循环
for (size_t i = 0; i < iter_times; ++i) {
do_something(i);
}

std::cout << std::endl;
[&]<std::size_t... p>(std::index_sequence<p...>) { (do_something(p), ...); }
(std::make_index_sequence<iter_times>{});

// 更进一步!
std::cout << std::endl;
[&]<std::size_t... p>(std::index_sequence<p...>) {
std::cout << (do_another_thing(p) + ...);
}
(std::make_index_sequence<iter_times>{});

return 0;
}

do_something()中,我们就是简单的把每次传进来的循环变量进行输出,而do_another_thing,则是把结果直接返回,方便我们进行计算(因为更进一步中的操作是想办法把迭代序列下标进行求和)。

1
2
3
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
45

可以看到我们完美的进行了循环转换。通过C++insight可以编译器形式的代码如下。

1
2
3
inline /*constexpr */ void operator()<0, 1, 2, 3, 4, 5, 6, 7, 8, 9>(std::integer_sequence<unsigned long, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9>) const {
std::cout.operator<<(do_another_thing.operator()(0UL) + (do_another_thing.operator()(1UL) + (do_another_thing.operator()(2UL) + (do_another_thing.operator()(3UL) + (do_another_thing.operator()(4UL) + (do_another_thing.operator()(5UL) + (do_another_thing.operator()(6UL) + (do_another_thing.operator()(7UL) + (do_another_thing.operator()(8UL) + do_another_thing.operator()(9UL))))))))));
}

总结

当你的结果不需要在运行时实时计算时,你可以试试利用这种模板元编程的技巧来将其转移到编译期,看起来还是非常cool并且非常有趣的,常规的for循环真的是已经写的麻木了。😋