C++ 中的 const 家族
C++ 里有三个"编译期求值"相关的关键字:constexpr、consteval、constinit。名字里都带 const,但职责各不相同——一个承诺"编译期可算",一个强制"只能在编译期算",一个解决"静态变量别出初始化顺序问题"。这篇把三者一次性梳理清楚。
一张表先建立全局印象
| 关键字 | 作用于 | 求值时机 | 能否修改 |
|---|---|---|---|
constexpr | 变量、函数 | 必须可在编译期求值 | 否 |
consteval | 函数 | 仅编译期,禁止运行期调用 | — |
constinit | 静态/线程存储期变量 | 编译期初始化 | 是(运行期可改) |
记住一句话:constexpr 管"编译期可算",consteval 管"只能在编译期算",constinit 管"静态变量别出初始化顺序坑"。
constexpr:编译期就能算出来
constexpr(constant expression)要求对象或函数必须能在编译期求值。相比只承诺"运行期不可改"的 const,它把计算推到了编译期。constexpr 函数尤其特别——它能在编译期和运行期两种语境下复用同一份定义,这就是下面要讲的“双态求值”。
const int runtime_val = get_value(); // const, but value may be runtime-known
constexpr int compile_val = 42; // must be known at compile time关键:双态求值
constexpr 函数最强大的地方在于它是双态(dual-mode)的——同一个函数体,编译器会根据调用点上下文决定在编译期还是运行期执行:
- 当所有实参都是常量表达式、且结果被用于需要常量表达式的语境(模板参数、
static_assert、constexpr变量初始化、数组大小等)时,函数在编译期求值; - 当实参包含运行期值,或调用点不需要常量结果时,函数退化为普通函数在运行期执行。
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// 编译期求值:实参是常量,且用于需要常量的语境
constexpr int f5 = factorial(5); // 120, folded at compile time
static_assert(f5 == 120);
int arr[factorial(3)]; // array size needs compile-time value → 6
// 运行期求值:实参是运行期值
int n = 0;
std::cin >> n;
int f = factorial(n); // computed at runtime, like a normal call
// 边界情况:实参是常量,但调用点不要求常量结果
int g = factorial(5); // 实参是常量 5,但 g 是普通变量
// 编译器“可以”在编译期算,但非强制
// 实际通常在编译期算出再写入,但这不是保证最后一种情况值得注意:constexpr 是“可以”在编译期算,不是“必须”。只有当实参是常量、且结果又被常量语境使用时,编译期求值才被强制;否则算在哪个阶段由实现决定(标准允许但不要求编译期折叠)。真正“必须在编译期算”是 consteval 的事。
这种双态带来一个巨大好处:一处定义,两处可用。你不需要像 C 那样写一份宏给编译期用、再写一份函数给运行期用,constexpr 函数自动适配两种调用语境:
// 这一份定义既服务于编译期,也服务于运行期
constexpr int hash_str(std::string_view s) {
int h = 0;
for (char c : s) h = h * 31 + c;
return h;
}
// 编译期:用于 switch-case 标签、模板参数
constexpr int id = hash_str("login");
// 运行期:处理用户输入的命令名
int cmd_hash = hash_str(user_command);正因为双态,constexpr 才能逐步“吃掉”越来越多原本只能运行期的逻辑。从 C++11 只能写 return 一条语句,到 C++14 允许循环和局部变量,到 C++20 放开 std::vector/std::string 等类型……标准一路放宽,本质就是让同一个函数在编译期语境里能干的事越来越多。这种放宽让你可以把运行期函数直接加个 constexpr 关键字,而不必为编译期另写一套——这是现代 C++ 把计算“上提”到编译期的核心机制。
constexpr 还能用在构造函数上,让类型在编译期构造:
struct Point {
int x, y;
constexpr Point(int x, int y) : x(x), y(y) {}
constexpr int dot(Point o) const { return x * o.x + y * o.y; }
};
constexpr Point a(1, 2), b(3, 4);
constexpr int d = a.dot(b); // 11, all at compile timeC++20 起放宽了 constexpr 的限制:可以用的语句越来越多,甚至能在 constexpr 函数里用 std::vector、std::string(C++20/26 逐步支持)。这让大量原本只能运行期的逻辑得以"上提"到编译期。
consteval:只能在编译期算
consteval(C++20)是 constexpr 的严格版本——它强制函数只能在编译期求值,禁止运行期调用。
consteval int square(int n) { return n * n; }
constexpr int a = square(5); // OK: compile time
int x = 5;
int b = square(x); // error: x is not a constant expression什么时候用 consteval?当你不希望这段代码在运行期执行时——比如:
- 编译期生成查找表、格式串校验、正则编译,避免运行期开销;
- 强制某段逻辑必须由编译器验证,而不是偷偷退化成运行期调用。
一个典型场景是格式串检查。C++20 的 std::format 系列会校验格式串,但只有把校验放在 consteval 函数里,才能保证错误在编译期报出,运行期零成本:
// simplified idea behind std::format runtime-check elimination
consteval void check_fmt(std::string_view fmt) {
// validate placeholders match argument count at compile time
}
template<typename... Args>
auto format_str(std::string_view fmt, Args&&... args) {
check_fmt(fmt); // forced compile-time check
return std::format(fmt, std::forward<Args>(args)...);
}一句话区分:
constexpr是"可以在编译期算",consteval是"只能在编译期算"。
constinit:编译期初始化,但能改
constinit(C++20)解决的是另一个问题:静态变量的初始化顺序。
具有静态存储期的变量(全局变量、函数内 static 变量、类的 static 成员)的初始化分两种:
- 常量初始化(constant initialization):编译期完成,零开销;
- 动态初始化(dynamic initialization):运行期执行初始化代码。
跨翻译单元的动态初始化顺序是未定义的,这就是臭名昭著的"静态初始化顺序灾难"(SIOF):A 的初始化依赖 B,但 B 还没初始化。
constinit 强制变量进行常量初始化,杜绝动态初始化,从而消除 SIOF:
// header
constinit int g_counter = 0; // compile-time init, but mutable
void inc() { ++g_counter; } // OK: can modify at runtime关键区别在于:constinit 不保证只读。它和 const 是正交的两个维度:
| 编译期初始化 | 运行期只读 | |
|---|---|---|
const | 不保证 | 是 |
constexpr | 是 | 是 |
constinit | 是 | 否 |
constinit const | 是 | 是 |
constinit int a = 0; // compile-time init, mutable
constexpr int b = 0; // compile-time init, read-only
constinit const int c = 0; // compile-time init, read-only用法口诀:需要"静态变量在编译期就初始化好、避免初始化顺序坑,但运行期还想改它"——用
constinit。
它们之间的关系
把这三个放在一起看,C++ 的"编译期求值"设计是分层的:
constexpr是编译期可算的通用承诺,覆盖变量和函数,且能在编译期/运行期双态运行;consteval把"可算"收紧为"必须算",专供编译期专用逻辑,禁止运行期退化;constinit关注的是静态存储期的初始化时机,和只读无关,是 SIOF 的解药。
选择上有个简单的决策树:
- 变量/函数编译期可算、运行期也能用 →
constexpr; - 函数只在编译期用,禁止运行期退化 →
consteval; - 静态变量怕初始化顺序、但运行期要改它 →
constinit。
// a class exercising all three
class Config {
public:
constexpr Config(int id, std::string_view name) : id_(id), name_(name) {}
[[nodiscard]] constexpr int id() const { return id_; }
consteval static Config default_config() {
return Config{0, "default"};
}
private:
int id_;
std::string_view name_;
};
constinit const Config g_config = Config{1, "app"}; // compile-time init, read-only
constexpr Config c_config = Config{2, "lib"}; // compile-time, read-only现代 C++ 的趋势是把尽可能多的计算和约束推到编译期——更早发现错误、更少运行期开销。constexpr/consteval/constinit 正是这条路线的核心工具。理解它们的分工,写出的代码会更安全、更高效,也更"有品味"。



