在C++20之后,语言提供了两种关键字——constexpr和consteval,它们都与常量表达式相关,但用途和语义并不相同。本文将从定义、编译时求值机制、使用场景以及实际代码示例等方面系统地阐述它们的区别,并给出一些实用的编程技巧。
1. 语义回顾
| 关键字 | 说明 | 作用域 | 编译时求值 |
|---|---|---|---|
constexpr |
声明一个表达式或函数在编译时可求值,除非在运行时显式调用 | 可在编译时或运行时使用 | 只要满足条件就可以在编译时求值 |
consteval |
声明一个函数必须在编译时调用,任何运行时调用都非法 | 只在编译时可用 | 必须在编译时求值,否则编译错误 |
要点
constexpr可以是一个常量,也可以是一个在编译时能求值的函数;若在运行时调用,它会像普通函数一样执行。consteval只定义编译时函数,强制编译器在所有调用点进行求值,运行时调用会触发编译错误。
2. constexpr 的细节
2.1 何时可以被求值
- 任何符合常量表达式规则的表达式:
constexpr int x = 5 + 3; constexpr函数在调用时如果所有实参都是常量表达式,编译器就会在编译期求值;如果不是,编译器会退回到运行时。
2.2 典型用例
-
编译期数组长度
constexpr std::size_t array_size() { return 10; } int arr[array_size()]; // 编译时确定长度 -
模板元编程
template <int N> struct factorial { static constexpr int value = N * factorial<N-1>::value; }; template <> struct factorial <0> { static constexpr int value = 1; }; -
编译期字符串拼接
constexpr const char* prefix = "Hello, "; constexpr const char* name = "World!"; constexpr const char* greet = prefix + name; // 仅在编译期拼接
2.3 constexpr 对类成员
- 成员函数可以声明为
constexpr,但必须满足所有成员都可以在编译时初始化的条件。 - 成员变量可以声明为
static constexpr,在类内部直接初始化。
3. consteval 的细节
3.1 强制编译期调用
consteval函数在任何调用点都必须是常量表达式。编译器若检测到运行时调用,直接报错。
3.2 典型用例
-
安全的编译期除法
consteval int safe_div(int a, int b) { if (b == 0) throw "division by zero"; return a / b; } constexpr int result = safe_div(10, 2); // OK int x = safe_div(10, input); // 编译错误:input 不是常量 -
编译期类型检查
template <typename T> consteval void require_integral() { static_assert(std::is_integral_v <T>, "T must be integral"); } require_integral <int>(); // OK require_integral <double>(); // 编译错误 -
编译期唯一 ID 生成
consteval unsigned int next_id() { static unsigned int counter = 0; return ++counter; } constexpr unsigned int id1 = next_id(); constexpr unsigned int id2 = next_id(); // id1=1, id2=2
3.3 与constexpr的差异
constexpr函数可以在运行时被调用;consteval函数则不行。constexpr函数可以声明为inline或constexpr inline,但consteval自动隐含inline。constexpr可以用作变量初始化、模板参数等;consteval只能用于函数。
4. 性能与可维护性
| 关键字 | 性能影响 | 可维护性 |
|---|---|---|
constexpr |
若满足编译期求值,性能提升;若不满足,等同普通函数 | 灵活,易维护 |
consteval |
必须在编译期,减少运行时成本 | 约束强,易导致误用 |
实践建议
- 对于需要强制编译期求值且可能导致运行时错误的逻辑,使用
consteval可以提升安全性。- 对于通用逻辑,可使用
constexpr,让编译器根据情况决定是否求值。
5. 示例:从constexpr到consteval
下面用一个简易的“编译期数学库”演示两者的使用差异:
#include <iostream>
#include <stdexcept>
#include <type_traits>
// ---------- constexpr 版本 ----------
constexpr int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
// ---------- consteval 版本 ----------
consteval int fib_safe(int n) {
if (n < 0) throw "negative argument";
if (n <= 1) return n;
return fib_safe(n-1) + fib_safe(n-2);
}
int main() {
constexpr int f10 = fib(10); // 编译时求值
std::cout << "fib(10) = " << f10 << '\n';
constexpr int f10_safe = fib_safe(10); // 编译时求值
std::cout << "fib_safe(10) = " << f10_safe << '\n';
// 以下会触发编译错误:fib_safe 必须在编译时调用
// int user_input = 5;
// int run_time = fib_safe(user_input); // 编译错误
// fib 在运行时也能工作
int run_input = 6;
int run_result = fib(run_input); // 编译时无法求值,运行时执行
std::cout << "fib(" << run_input << ") = " << run_result << '\n';
}
运行结果:
fib(10) = 55
fib_safe(10) = 55
fib(6) = 8
6. 小结
constexpr:提供编译期求值的可能性,保持灵活性;若不满足编译期条件,退回到运行时。consteval:强制编译期调用,保证在所有调用点都是常量表达式;违反即报错,提升安全性。- 在需要高度可预测、无运行时开销的场景(如元编程、硬件描述、编译期配置)下,优先考虑
consteval。 - 对于通用函数、需要兼容运行时的代码,使用
constexpr更合适。
通过合理选择这两个关键字,可以让 C++ 代码在编译期完成更多计算,提升运行时性能,同时增强代码的自我检查能力。