关于编译期计算,直接能够想到的应用是决定是否启用某个模板,或者多个模板之间做选择。但如果有足够多的信息,编译器甚至可以计算控制流的结果。
模板元编程
模板元编程的简单例子,如下:
cpp
#include <iostream>
template <unsigned p, unsigned d>
struct DoIsPrimer {
static constexpr bool value = (p % d != 0) && DoIsPrimer<p, d - 1>::value;
};
template <unsigned p>
struct DoIsPrimer<p, 2> {
static constexpr bool value = (p % 2 != 0);
};
template <unsigned p>
struct IsPrimer {
static constexpr bool value = DoIsPrimer<p, p / 2>::value;
};
template <>
struct IsPrimer<0> { static constexpr bool value = false; };
template <>
struct IsPrimer<1> { static constexpr bool value = false; };
template <>
struct IsPrimer<2> { static constexpr bool value = true; };
template <>
struct IsPrimer<3> { static constexpr bool value = true; };
//仅为证明自己算发生在编译阶段
template <unsigned p, typename enable = typename std::enable_if<IsPrimer<p>::value>::type>
class Primer {
};
int main(int argc, char **argv)
{
Primer<3> primer3;
Primer<9> primer9;
return 0;
}
上面的例子Primer<3> primer3;通过编译,Primer<9> primer9;编译报错,这足以证明IsPrimer在编译阶段完成了计算,其展开步骤如下:
bash
IsPrime<9>::value
=> DoIsPrime<9,4>::value
=> 9%4!=0 && DoIsPrime<9,3>::value
=> 9%4!=0 && 9%3!=0 && DoIsPrime<9,2>::value
=> 9%4!=0 && 9%3!=0 && 9%2!=0
=> false
通过 constexpr 进行计算
c++11开始引入了constexpr特性,大大简化了编译器运算。但对于constexpr使用,c++11拥有诸多限制,如constexpr的定义只能包含一个return语句。这些限制从c++14开始,大部分被移除。但为了所有计算步骤都能够在编译其进行,目前所有的c++版本constexpr函数都不支持异常抛出和内存分配。下面是constexpr版的IsPrimer:
cpp
#include <iostream>
#if 1
//c++11实现
constexpr bool DoIsPrimer(unsigned p, unsigned d) { return d != 2 ? (p % d != 0) && DoIsPrimer(p, d - 1) : (p % 2 != 0); }
constexpr bool IsPrimer(unsigned p) { return p < 4 ? !(p < 2) : DoIsPrimer(p, p / 2); }
#else
//c++14实现
constexpr bool IsPrimer(unsigned p) {
for (unsigned d = 2; d < p / 2; ++d) {
if (p % d == 0)
return false;
}
return p > 1;
}
#endif
//仅为证明自己算发生在编译阶段
template <unsigned p, typename enable = typename std::enable_if<IsPrimer(p)>::type>
class Primer {
};
int main(int argc, char **argv)
{
Primer<3> primer3;
Primer<9> primer9;
return 0;
}
需要注意一点,"可以"在编译期计算,并非"一定"在编译期计算。计算发生在编译还是运行,先来做一个实验:
cpp
//...
bool is_p0 = IsPrimer(0); //==在编译期计算
const bool is_p1 = IsPrimer(1); //==在编译期计算
constexpr bool is_p2 = IsPrimer(2); //==在编译期计算
int p3 = 3;
bool is_p3 = IsPrimer(p3); //在运行期计算
int p4 = 4;
const bool is_p4 = IsPrimer(p4); //在运行期计算
const int p5 = 5;
constexpr bool is_p5 = IsPrimer(p5); //==在编译期计算
int main(int argc, char **argv)
{
Primer<3> primer3;
bool is_p6 = IsPrimer(6); //在运行期计算
int p7 = 7;
bool is_primer = IsPrimer(p7); //在运行期计算
const int p8 = 8;
constexpr bool is_p8 = IsPrimer(p8); //==在编译期计算
return 0;
}
关于上面的实验, 通过在IsPrime设置断点即可判断计算发生在编译期还是运行期。通过实验,可以发现下面的规律:
- 如果计算在全局,结果变量为constexpr或输入参数为常量,计算都会在编译期触发
- 如果计算在局部,结果变量为constexpr并且输入参数为常量,计算才会在编译期触发,否则延迟到运行期
编译器if
c++17引入了if constexpr(...)语法,编译器根据该语法在编译期决定使用if部分还是else部分的代码。该语法主要用于两个场景:
- 变参模板
使用编译器if判断参数数量,当参数为0时,不再执行任何语句,解除对void print(void)函数的依赖。此处编译期if 不可被普通if 取代,print函数族的迭代生成实在编译器完成的,如果使用普通if,编译时会报错"No matching function for call to 'print' ",因为print模板不会生成参数为0的函数,当编译迭代到...args为0时,发现没有合适的函数调用报错。
cpp
#include <iostream>
template <typename T, typename ...Ts>
void print(const T &arg, const Ts &...args)
{
std::cout << arg << std::endl;
if constexpr(sizeof...(args) > 0) {
print(args...);
}
}
int main(int argc, char **argv)
{
print("Jim", 'M', 30);
return 0;
}
- std::is_xxx函数族
通过使用std::is_xxx函数族判断,不同的的情况使用不同的代码。下面是一个例子,如果T是整形族,则执行递归;否则直接返回原值。此处是否可用普通if 语句代替编译器if 语句?答案是否定的。如果使用普通if语句,编译时,整个函数的所有语句都会被编译,当类型为string时,由于不支持和整型的比较,及-和*操作符,arg == 1和arg * factorial(arg - 1)都会报错。
cpp
template <typename T>
constexpr T factorial(T arg)
{
if constexpr(std::is_integral_v<T>) {
if (arg == 1)
return arg;
return arg * factorial(arg - 1);
}
else {
///static_assert(false, "argument is not integral!"); ///编译期总是会被触发,不管是否会用到该段代码
static_assert(!std::is_integral_v<T>, "argument is not integral!"); //总是不会被触发,即便该段代码被用到
std::cout << arg<< std::endl;
return arg;
}
}
int main(int argc, char **argv)
{
std::cout << factorial(5) << std::endl;
std::cout << factorial("hello") << std::endl;
return 0;
}