C++常量表达式(constexpr)

关于常量表达式

关键字constexpr是在 C++11 引入的,首先和 const 一样可以用于修饰变量,这时候相当于一个加强版的 const,该变量具备常量属性(也即不可修改),且它的值必须在编译阶段就已知。不同的是,constexpr 还可以用于修饰函数(称为constexpr函数),这个函数在传入参数都是常量表达式时能够返回编译期常量,若参数在编译期未知时则和普通函数一样在运行期计算。

constexpr 修饰变量

constexpr 修饰变量时,这个变量具备const属性,也就是不能够修改这个变量的值,其次还说明这个变量的值必须在编译期就已知。这个编译期指的是预处理、编译、链接的整个过程,在编译阶段就已知的变量可以放在只读内存中,可以用于作为数组大小参数等。

1
2
3
4
5
6
7
int sz = 5;
constexpr int arraySize1 = sz; // 错误,必须使用编译期已知量初始化
std::array<int, sz> data1; // 错误,数组大小模板参数要求常量表达式

constexpr int arraySize2 = 10; // ok
std::array<int, arraySize2> data2; // ok
int nums[arraySize2]; // ok

再看关键词constexprconst的区别,从语义上来说,const修饰的变量只说明这个变量的常量性,也就是不可直接修改这个变量的值,并没有保证这个变量一定是在编译期初始化的,而constexpr则指定了变量必须在编译期初始化。

1
2
3
4
5
int sz;
const int arraySize = sz; // ok,arraySize将在运行期进行初始化,且arraySize是一个常量

constexpr int arraySize1 = sz; // 错误,arraySize1 必须在编译期确定
std::array<int, arraySize> data; // 错误,arraySize 必须在编译期确定

总的来说,所有constexpr对象都是const对象,而const对象并不一定是constexpr对象。

constexpr 修饰函数

constexpr 修饰函数必须要有返回值(返回值不能为 void),这样的函数在传入的都是编译期常量的情况下,返回一个编译期常量;在传入的参数中含有非编译期常量的情况下,该函数和普通函数一样在运行时产生一个值。这样免得对同一函数编写constexpr版本和非constexpr版本。

例如我们要实现一个计算幂的常量表达式函数:

1
2
3
4
5
6
constexpr int pow(int base, int exp) {
...
}

constexpr auto numConds = 5;
std::array<int, pow(3, numConds)> results;

在 C++ 11 和 C++ 14 中,对使用 constexpr 修饰的函数实现有不同的限制,在 C++11 中限制该函数只能含有一条 return 语句,但是可以使用递归函数,对于上面的例子就可以实现如下:

1
2
3
constexpr int pow(int base, int exp) {
return (exp == 0 ? 1 : base * pow(base, exp - 1));
}

在 C++14 中,上述标准大大放宽,constexpr 修饰的函数中可以含有循环、条件判断等语句,上述函数可以有一种更易懂的实现:

1
2
3
4
5
6
7
constexpr int pow(int base, int exp) {
auto result = 1;
for (int i = 0; i < exp; ++i) {
result *= base;
}
return result;
}

constexpr修饰的函数在传入参数都是编译期常量时也会返回一个编译期常量,这个编译期常量包括所有的内置类型(除void外),实际上用户自定义的类型也可以是编译期常量,只需要将这个类的构造函数使用constexpr进行修饰。

constexpr 修饰构造函数

为了使用户自定义类型成为编译期常量,可以将其构造函数使用constexpr关键字修饰,只要传入构造函数的参数是编译期常量,那么这个类也是编译期常量:

1
2
3
4
5
6
7
8
9
10
class Point{
public:
constexpr Point(double xVal = 0, double yVal = 0) : x(xVal), y(yVal) {}
constexpr double xValue() const { return x; }
constexpr double yValue() const { return y; }
void setX(double newX) { x = newX; }
void setY(double newY) { y = newY; }
private:
double x, y;
}

只要传入构造函数的参数是编译期常量,那么这个类就是编译期常量:

1
constexpr Point t(1.0, 2.0);   // ok, 编译期常量

可以看到,我们将 Point 类的两个访问元素的成员函数也使用了constexpr修饰,只要类实例是编译期常量,其内部两个成员的值也是编译期常量,这两个函数返回编译期常量也是合情合理。

因此我们可以写出下面返回编译期常量的代码:

1
2
3
4
5
6
constexpr Point midpoint(const Point& p1, const Point& p2) {
return { (p1.xValue() + p2.xValue()) / 2,
(p1.yValue() + p2.yValue()) / 2};
}

constexpr auto mid = midpoint(p1, p2);

上面的函数涉及了 Point 类的构造、成员函数的访问,却仍然是一个 constexpr 函数,也就是说上述计算过程都可以在编译期就完成,这样的话软件运行速度就会提高。

实际上,在 C++14 中,setX()setY() 函数也可以使用 constexpr 修饰。这里不过多描述了,实际上,代码中使用 constexpr 修饰的代码越多,就会将越多的计算工作放到编译期执行,软件运行时的速度就会越快,当然编译的速度会变慢。

为了追求运行时的速度,可以尽可能的在需要使用编译期常量的情景都使用 constexpr 修饰。

参考文献

  1. 《Effective Mordern C++》