Compile Loop!
编译时计算
C++的一大好处就是可以在编译时进行一些非常有意思的操作。虽然频繁的编译时计算会显著的增加编译时间,但是同样的也会减小运行时负担。当然前面这个只不过是一方面的说辞,我使用模板或者constexpr这些特性纯粹是因为这样看起来比较有趣。最近我就从Bisqwit的一个视频中学到了一个将循环转到编译时的一个小技巧。故水个博客记录一下。
所有的测试代码可以在这里找到。
1 | constexpr int iter_times = 10; |
上面的这段代码是一段稀松平常的循环,我们利用for语句进行了10次循环,每次循环都去执行do_something()
这个函数。首先我们先考虑怎么把这个东西用编译时循环进行展开。我们所使用的到工具叫做std::index_sequence
以及std::make_index_sequence
,具体的代码如下所示。
1 | constexpr int iter_times = 10; |
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 | /* |
在do_something()
中,我们就是简单的把每次传进来的循环变量进行输出,而do_another_thing
,则是把结果直接返回,方便我们进行计算(因为更进一步中的操作是想办法把迭代序列下标进行求和)。
1 | 0 1 2 3 4 5 6 7 8 9 |
可以看到我们完美的进行了循环转换。通过C++insight可以编译器形式的代码如下。
1 | 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 { |
总结
当你的结果不需要在运行时实时计算时,你可以试试利用这种模板元编程的技巧来将其转移到编译期,看起来还是非常cool并且非常有趣的,常规的for循环真的是已经写的麻木了。😋