预计算

当编译器可以推断某个变量不依赖于任何用户提供的数据时,他们可以在编译时计算其值,并将其嵌入生成的机器代码,转化为常量。

这种优化对性能有很大帮助,但它不是C++标准的一部分,所以编译器不必这么做。当编译时计算很难实现或时间密集时,编译器可能会错过这个机会。

常量表达式

一个更可靠的解决方案,在现代C++中,可以将函数标记为constexpr;如果它是通过传递常量来调用的,则其值保证在编译时计算:

constexpr int fibonacci(int n) {
if (n <= 2)
return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}

static_assert(fibonacci(10) == 55);

这些函数有一些限制,比如它们只能调用其他constexpr函数,不能进行内存分配,但除此之外,它们还是会“按原样”执行。

请注意,虽然constexpr函数在运行时不会花费任何费用,但它们仍然会增加编译时间,因此至少还是要关心一下它们的效率,不要在其中放入非确定性多项式完全问题(NP-Complete,NPC)的东西:

constexpr int fibonacci(int n) {
int a = 1, b = 1;
while (n--) {
int c = a + b;
a = b;
b = c;
}
return b;
}

在早期的C++标准中,曾经有更多的限制,比如你不能在其中使用任何声明,必须依赖递归,所以整个过程感觉更像是Haskell编程(一种函数式编程语言),而不是C++。自从C++17以来,你甚至可以使用强制方式计算静态数组,这对于预计算查找表非常有用:

struct Precalc {
int isqrt[1000];

constexpr Precalc() : isqrt{} {
for (int i = 0; i < 1000; i++)
isqrt[i] = int(sqrt(i));
}
};

constexpr Precalc P;

static_assert(P.isqrt[42] == 6);

请注意,当你在调用constexpr函数时传递非常量,编译器在编译时可能会也可能不会计算它们:

for (int i = 0; i < 100; i++)
cout << fibonacci(i) << endl;

在这个例子中,尽管我们执行恒定次数的迭代,并使用编译时已知的参数调用fibonacci,但从技术上来说,它们不是编译时常数。是否优化这个循环取决于编译器——对于繁重的计算,编译器通常选择不优化。