我们已经连续讲了几讲比较累人的编译期编程了。今天我们还是继续这个话题,但是,相信今天学完之后,你会感觉比之前几讲要轻松很多。C++ 语言里的很多改进,让我们做编译期编程也变得越来越简单了。
初识 constexpr
我们先来看一些例子:
int sqr(int n)
{
return n * n;
}
int main()
{
int a[sqr(3)];
}
想一想,这个代码合法吗?
看过之后,再想想这个代码如何?
int sqr(int n)
{
return n * n;
}
int main()
{
const int n = sqr(3);
int a[sqr(3)];
}
还有这个?
#include <array>
int sqr(int n)
{
return n * n;
}
int main()
{
std::array<int, sqr(3)> a;
}
此外,我们前面模板元编程里的那些类里的 static const int 什么的,你认为它们能用在上面的几种情况下吗?
如果以上问题你都知道正确的答案,那恭喜你,你对 C++ 的理解已经到了一个不错的层次了。但问题依然在那里:这些问题的答案不直观。并且,我们需要一个比模板元编程更方便的进行编译期计算的方法。
在 C++11 引入、在 C++14 得到大幅改进的 constexpr 关键字就是为了解决这些问题而诞生的。它的字面意思是 constant expression,常量表达式。存在两类 constexpr 对象:
constexpr 变量(唉……😓)
constexpr 函数
一个 constexpr 变量是一个编译时完全确定的常数。一个 constexpr 函数至少对于某一组实参可以在编译期间产生一个编译期常数。
注意一个 constexpr 函数不保证在所有情况下都会产生一个编译期常数(因而也是可以作为普通函数来使用的)。编译器也没法通用地检查这点。编译器唯一强制的是:
constexpr 变量必须立即初始化
初始化只能使用字面量或常量表达式,后者不允许调用任何非 constexpr 函数
constexpr 的实际规则当然稍微更复杂些,而且随着 C++ 标准的演进也有着一些变化,特别是对 constexpr 函数如何实现的要求在慢慢放宽。要了解具体情况包括其在不同 C++ 标准中的限制,可以查看参考资料 [1]。下面我们也会回到这个问题略作展开。
拿 constexpr 来改造开头的例子,下面的代码就完全可以工作了:
#include <array>
constexpr int sqr(int n)
{
return n * n;
}
int main()
{
constexpr int n = sqr(3);
std::array<int, n> a;
int b[n];
}
要检验一个 constexpr 函数能不能产生一个真正的编译期常量,可以把结果赋给一个 constexpr 变量。成功的话,我们就确认了,至少在这种调用情况下,我们能真正得到一个编译期常量。
constexpr 和编译期计算
上面这些当然有点用。但如果只有这点用的话,就不值得我专门来写一讲了。更强大的地方在于,使用编译期常量,就跟我们之前的那些类模板里的 static const int 变量一样,是可以进行编译期计算的。
以 [第 13 讲] 提到的阶乘函数为例,和那个版本基本等价的写法是:
constexpr int factorial(int n)
{
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
然后,我们用下面的代码可以验证我们确实得到了一个编译期常量:
int main()
{
constexpr int n = factorial(10);
printf("%d\n", n);
}
编译可以通过,同时,如果我们看产生的汇编代码的话,一样可以直接看到常量 3628800。
这里有一个问题:在这个 constexpr 函数里,是不能写 static_assert(n >= 0) 的。一个 constexpr 函数仍然可以作为普通函数使用——显然,传入一个普通 int 是不能使用静态断言的。替换方法是在 factorial 的实现开头加入:
if (n < 0) {
throw std::invalid_argument(
"Arg must be non-negative");
}