跳到主要内容

C++ 11 constexpr

引言

在C++11之前,我们虽然可以使用宏和模板元编程进行编译期计算,但这些方法要么不安全(宏),要么难以编写和理解(模板元编程)。C++11引入了constexpr关键字,它使我们能够更自然地编写可在编译期执行的函数和初始化常量表达式,从而提升程序的性能和安全性。

本文将为你详细介绍constexpr的基本概念、语法规则、使用场景以及实际应用示例,让你能够轻松地在自己的程序中应用这一强大特性。

什么是constexpr?

constexpr是C++11引入的关键字,它可以用来修饰变量、函数、构造函数等,表明它们可以(但不必须)在编译期求值。

  • constexpr变量:必须用常量表达式初始化的常量。
  • constexpr函数:在某些情况下可以在编译期求值的函数。
  • constexpr构造函数:允许在编译期创建对象的构造函数。
提示

constexpr不同于constconst主要表示"不可修改",而constexpr表示"可以在编译期求值"。所有constexpr变量都是const的,但反之不成立。

constexpr变量

constexpr变量必须使用常量表达式进行初始化:

cpp
constexpr int square_size = 8;               // 正确:直接使用字面量
constexpr double pi = 3.14159265358979323846; // 正确:使用字面量
constexpr int array_size = square_size * 2; // 正确:使用constexpr表达式

int runtime_value = get_value(); // 运行时值
constexpr int error = runtime_value; // 错误:初始化必须是常量表达式

constexpr函数

基础用法

constexpr函数是可以在编译期被求值的函数,但前提是它的参数都是常量表达式。以下是一个计算阶乘的constexpr函数:

cpp
constexpr int factorial(int n) {
return (n <= 1) ? 1 : (n * factorial(n - 1));
}

int main() {
constexpr int result1 = factorial(5); // 编译期计算
std::cout << "5! = " << result1 << std::endl; // 输出:5! = 120

int n = 4;
int result2 = factorial(n); // 运行时计算
std::cout << "4! = " << result2 << std::endl; // 输出:4! = 24

return 0;
}

C++ 11对constexpr函数的限制

在C++11中,constexpr函数有较多限制:

  1. 函数体只能包含一条return语句
  2. 不能使用局部变量
  3. 不能使用循环和switch语句
  4. 不能有副作用(如修改全局变量)
  5. 不能使用try-catch块

以下是一个符合C++11限制的constexpr函数:

cpp
constexpr int max(int a, int b) {
return (a > b) ? a : b; // 单条return语句
}
备注

C++14中放宽了对constexpr函数的限制,允许使用局部变量、循环等。

constexpr构造函数

constexpr构造函数允许在编译期创建类的实例:

cpp
class Point {
private:
int x, y;
public:
constexpr Point(int x_val, int y_val) : x(x_val), y(y_val) {}
constexpr int getX() const { return x; }
constexpr int getY() const { return y; }
constexpr int sum() const { return x + y; }
};

int main() {
constexpr Point p1(5, 3);
constexpr int sum = p1.sum(); // 编译期计算

std::cout << "Point coordinates: (" << p1.getX() << ", " << p1.getY() << ")" << std::endl;
std::cout << "Sum: " << sum << std::endl;

return 0;
}

输出:

Point coordinates: (5, 3)
Sum: 8

constexpr的好处

  1. 性能提升:编译期计算减少了运行时开销。
  2. 安全性:编译期错误比运行时错误更容易被发现和修复。
  3. 可读性:相比模板元编程,constexpr代码更自然、更易于理解。
  4. 代码简化:可以替代某些需要宏和模板元编程才能实现的功能。

实际应用场景

1. 数组大小

cpp
constexpr int calculate_buffer_size(int base_size) {
return base_size * 2 + 10;
}

int main() {
constexpr int buffer_size = calculate_buffer_size(20);
int buffer[buffer_size]; // C++允许使用constexpr变量作为数组大小

// 使用buffer...

return 0;
}

2. 查找表和数学常量

cpp
constexpr double power(double base, int exponent) {
return (exponent == 0) ? 1.0 : base * power(base, exponent - 1);
}

int main() {
// 编译期创建查找表
constexpr double powers_of_2[10] = {
power(2.0, 0), power(2.0, 1), power(2.0, 2), power(2.0, 3), power(2.0, 4),
power(2.0, 5), power(2.0, 6), power(2.0, 7), power(2.0, 8), power(2.0, 9)
};

for (int i = 0; i < 10; ++i) {
std::cout << "2^" << i << " = " << powers_of_2[i] << std::endl;
}

return 0;
}

输出:

2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16
2^5 = 32
2^6 = 64
2^7 = 128
2^8 = 256
2^9 = 512

3. 编译期计算圆周率近似值

cpp
constexpr double square(double x) {
return x * x;
}

constexpr double pi_approximation() {
// 使用莱布尼茨级数计算π的近似值
double sum = 0.0;
for (int i = 0; i < 1000; ++i) { // 注意:这只在C++14及以上版本有效
double term = 1.0 / (2 * i + 1);
if (i % 2 == 0)
sum += term;
else
sum -= term;
}
return 4.0 * sum;
}

int main() {
constexpr double pi = pi_approximation(); // 编译期计算π
constexpr double circle_area = pi * square(5.0); // 编译期计算圆面积

std::cout << "Pi approximation: " << pi << std::endl;
std::cout << "Area of circle with radius 5: " << circle_area << std::endl;

return 0;
}
警告

上面的pi_approximation函数在C++11中无效,因为它包含循环和局部变量。这是一个C++14的例子。

注意事项与限制

  1. 不一定在编译期求值:即使标记为constexpr,如果条件不满足(例如参数不是常量表达式),函数也会在运行时求值。

  2. 不能有副作用constexpr函数不能修改全局状态或进行I/O操作。

  3. 递归深度限制:编译器对编译期递归有深度限制。

  4. 浮点精度:编译期浮点计算可能与运行时计算结果略有不同,具体取决于编译器实现。

如何检查是否进行了编译期求值?

不幸的是,C++标准没有提供直接方法来确认constexpr函数是否在编译期求值。一种常见的"检查"方法是:

cpp
// 这个数组声明只有在factorial(5)能在编译期求值时才有效
constexpr int result = factorial(5);
int array[result]; // 如果factorial(5)不是常量表达式,这里会编译失败

总结

constexpr是C++11引入的一个强大特性,它允许我们编写可在编译期执行的代码,从而提高程序的性能和安全性。通过本文,我们了解了:

  • constexpr变量必须用常量表达式初始化,并且它们自身也是常量表达式
  • constexpr函数在参数为常量表达式时可以在编译期求值
  • C++11对constexpr函数有严格限制,而C++14放宽了这些限制
  • constexpr构造函数允许在编译期创建类的实例
  • constexpr可以应用于数组大小定义、查找表生成、物理/数学常量计算等场景

随着C++标准的发展,特别是C++14、C++17和C++20,constexpr的能力不断增强,使我们能够在编译期执行更复杂的计算,进一步提升程序的效率和质量。

练习

  1. 编写一个constexpr函数计算斐波那契数列的第n项。
  2. 创建一个带有constexpr构造函数的矩形类,并提供计算面积和周长的constexpr方法。
  3. 使用constexpr函数生成一个包含前10个质数的数组。
  4. 尝试编写一个constexpr函数,计算给定精度下的e(自然对数的底)的近似值。

进一步学习资源

  • cppreference: constexpr specifier
  • C++14和C++17中的constexpr扩展
  • 模板元编程与constexpr的结合使用
  • if constexpr(C++17特性)
  • constevalconstinit(C++20特性)

通过掌握constexpr,你将能够编写更高效、更安全的C++程序,充分利用编译期计算的优势!