C++ constexpr、consteval、constinit

标准版本 特点
C++11 引入 constexpr 变量和函数;函数体限单条 return 语句
C++14 放宽 constexpr 函数限制:允许局部变量、多个 return、循环等
C++17 引入 if constexprconstexpr static 成员变量可内联初始化;允许 constexpr Lambda
C++20 允许 constexpr 虚函数、try-catchdynamic_casttypeid;引入 constevalconstinit

一、什么是 constexpr

C++11 中新增加了用于指示常量表达式的 constexpr 关键字。

constexpr 表示可以在编译器求值(但并不强制)。它允许把表达式/函数/构造放在编译器计算,从而用于需要常量表达式的场景(如数组大小、模板参数、static_assert 等)。

constexpr 修饰函数/构造函数时,表示该函数可以在编译期被调用;在运行时也可以被正常调用。若你需要强制在编译期求值,用 consteval (C++20)。

C++17 引入了 if constexpr(编译期分支),C++20 又加入了 constevalconstinit 等扩展,整体上 constexpr 能做的事情随着 C++ 标准演进越来越多。

基本语法与要求:

1
2
3
4
5
6
7
8
9
constexpr int maxSize = 100;           // OK: 字面量初始化
constexpr double pi = 3.14159265359; // OK

// 错误示例
int n = 10;
// constexpr int size = n; // ERROR: n 不是常量表达式

const int m = 20;
constexpr int size = m; // OK: const int 是常量表达式

二、constexpr 与 const

不过要注意区分 constconstexpr

修饰对象的时候两者之间最基本的区别是:

  • const 修饰一个对象表示它是常量。这表示对象一经初始化就不会再变动了,并且允许编译器使用这个特点优化程序。这也防止程序员修改了本不应该修改的对象。
  • constexpr 是修饰一个常量表达式。但请注意 constexpr 不是修饰常量表达式的唯一途径。
1
2
3
4
5
6
7
8
// constexpr 变量 "隐式 const"(C++11-C++17,C++20 有变化)
constexpr int x = 10; // 等价于 const int x = 10;
// 但反过来不成立!

// C++20 起 constexpr 函数参数可以是 const(之前不可)
constexpr int add(const int a, const int b) { // C++20
return a + b;
}

修饰函数的时候两者之间最基本的区别是:

  • const 只能用于非静态成员的函数而不是所有函数。它保证成员函数不修改任何非静态数据。
  • constexpr 可以用于含参和无参函数。constexpr 函数适用于常量表达式,只有在下面的情况下编译器才会接受constexpr 函数:
    1. 函数体必须足够简单,除了 typedef 和静态元素,只允许有 return 语句。如构造函数只能有初始化列表,typedef 和静态元素 (实际上在 C++14 标准中已经允许定义语句存在于 constexpr 函数体内了)
    2. 参数和返回值必须是字面值类型
1
2
3
constexpr int square(int x) { return x * x; };

constexpr int val = square(4); // 编译期就已知val==16
特性 const constexpr
求值时机 运行期(或编译期,不保证) 保证编译期
编译期常量 不一定是 一定是
用途 只读语义 编译期计算、数组大小、模板参数等
变量修饰 可修饰运行时变量 只能修饰编译期可确定的对象
函数修饰 N/A 函数可在编译期执行

三、constexpr 的使用

3.1 基本用法

constexpr 放在函数或构造函数前,表示函数可以在常量表达式上下文被求值。调用该函数时如果参数都是常量表达式,编译器会在编译期执行它并产生结果。

重要一点:constexpr 并不强制编译期求值——如果传入的是运行时值,函数会在运行时执行 (行为像普通函数)。如果你需要强制编译期求值,使用 consteval (见下)。

1
2
3
4
5
6
7
constexpr int factorial(int n) {
return n <= 1 ? : (n * factorial(n - 1));
}

static_assert(factorial(5) == 120); // 在编译期验证
int x = 6;
int r = factorial(x); // 运行时调用(如果 x 不是常量表达式)

3.2 使用注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 陷阱 1:误给非编译期对象
int runtime_n;
// constexpr int bad = runtime_n; // ERROR

// 陷阱 2:constexpr 函数中不能调用非 constexpr 函数
int runtimeFunc() { return 42; }
constexpr int badFunc() {
// return runtimeFunc(); // ERROR
return 42;
}

// 陷阱 3:constexpr 指针的限制
int global = 10;
constexpr int* p1 = &global; // OK: 指向静态存储期
void f() {
int local = 20;
// constexpr int* p2 = &local; // ERROR: 局部变量地址非编译期常量
}

// 陷阱 4:容器操作的误区
constexpr std::vector<int> v{1,2,3}; // ERROR: vector 非 LiteralType (C++20前)
// 解决方案:std::array, std::span, 或自定义 constexpr 容器

// 陷阱 5:隐式 const 的误解
void func(const int& x); // 接受任何 int
void func(int& x); // 非 const 引用

constexpr int val = 10;
func(val); // 调用 const int& 版本(constexpr 隐式 const)

// 陷阱 6:编译期无限递归
constexpr int bad_recursion(int n) {
return bad_recursion(n); // 编译错误:无限递归
}

// 陷阱 7:虚函数 constexpr 的对象切片
struct Base { virtual constexpr ~Base() = default; };
struct Derived : Base {};

constexpr Derived d;
constexpr Base b = d; // 切片!virtual 表被切掉
// constexpr const Base& ref = d; // OK: 引用避免切片

四、各版本演进

constexpr 函数的演进是 C++ 编译期计算能力扩张的核心。

4.1 C++11

C++11 的 constexpr 限制比较多,几乎只能是用于纯函数式的:

1
2
3
4
5
6
7
8
// C++11: 函数体必须是单条 return 语句
constexpr int factorial_cpp11(int n) {
return (n <= 1) ? 1 : (n * factorial_cpp11(n - 1)); // 递归是唯一选择
}

// 应用
constexpr int fact5 = factorial_cpp11(5); // 编译期计算 120
// int runtime = factorial_cpp11(n); // 也可以运行时调用

限制

  • 不能声明变量
  • 不能使用 ifforwhile(只能用 ?: 和递归)
  • 不能有 void 返回(C++11 标准原文如此,实际有例外)

4.2 C++14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// C++14: 真正的编译期函数式编程
constexpr int factorial_cpp14(int n) {
int result = 1; // OK: 允许局部变量
for (int i = 2; i <= n; ++i) { // OK: 允许循环
result *= i;
}
return result;
}

constexpr int gcd(int a, int b) { // 更直观的欧几里得算法
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}

4.3 C++17

1
2
3
4
5
6
7
8
9
10
11
// C++17: constexpr 允许更复杂的控制流
constexpr int complexLogic(int n) {
if (n < 0) return 0; // if 语句(C++14 已有)

// 可以调用其他 constexpr 函数,包括标准库部分函数
switch (n) { // switch 也可以
case 0: return 1;
case 1: return 2;
default: return n * 2;
}
}

C++17 起,Lambda 在适当条件下隐式成为 constexpr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C++17: 隐式 constexpr(如果满足条件)
constexpr auto square = [](int n) { return n * n; };
constexpr int nine = square(3); // OK: 9

// C++20: 显式 constexpr Lambda
constexpr auto add = [](int a, int b) constexpr { // 显式标记
return a + b;
};

// 泛型 Lambda + constexpr
auto generic = [](auto a, auto b) {
if constexpr (std::is_same_v<decltype(a), decltype(b)>) {
return a + b;
}
};

4.4 C++20

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// C++20: constexpr 虚函数(运行时多态的编译期版本)
struct Base {
virtual constexpr int getValue() const { return 1; }
virtual constexpr ~Base() = default; // 虚析构也可以是 constexpr
};

struct Derived : Base {
constexpr int getValue() const override { return 2; }
};

constexpr int callVirtual(const Base& b) {
return b.getValue(); // 编译期多态!
}

constexpr Derived d;
constexpr int val = callVirtual(d); // val == 2

// C++20: constexpr 异常处理
constexpr int safeDivide(int a, int b) {
if (b == 0) throw std::runtime_error("Division by zero");
return a / b;
}

// C++20: constexpr 内存分配(特定条件)
constexpr int* makeArray() {
int* p = new int[5]{1,2,3,4,5}; // 编译期 new/delete!
int sum = p[0] + p[4];
delete[] p;
return sum;
}

4.4.1 consteval——立即函数(强制编译期)

consteval 表示立即函数:函数被标记为 consteval 式,所有可能被求值的调用必须在编译期产生常量表达式,否则就是编译错误。也就是说,调用时强制在编译期执行(不能在运行时执行)。

这是 constexpr 的补充:constexpr 时可以在编译期求值,而 consteval 时必须在编译期求值。

1
2
3
consteval int make_magic() { return 42; }
const expr int id = make_magic(); // OK
int x = make_magic(); // 编译错误,不能在运行时调用 consteval 函数

4.4.2 constinit——与静态初始化顺序相关

constinit 并不会使变量成为常量 (它并不隐含 const),它的目的是保证具有静态/线程存储期的变量进行静态初始化而不是动态初始化;如果其初始化不是常量初始化,程序就是 informed (违例)。因此 constinit 常用于防止静态初始化顺序混乱(static initialization order fiasco)。简而言之:constinit 是关于“初始化时机”的断言,而非关于是否常量。

1
2
constexpr int compile_time_val = 10; // 常量初始化
constinit int runtime_global = compile_time_val; // OK(保证为常量初始化)

注意:constexpr 隐含 constinit 的初始化要求;但 constinit 本身不隐含 const

五、constexpr if (C++17)

if constexpr(cond) 是编译期分支,不是运行时 if,用于替代冗长的 SFINAE 和模板特化。

5.1 基本语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T>
auto getValue(T t) {
if constexpr (std::is_pointer_v<T>) { // 编译期分支
return *t; // 如果 T 不是指针,这行被 discard
} else {
return t;
}
}

int i = 10;
int* p = &i;

auto v1 = getValue(i); // T=int, 返回 int
auto v2 = getValue(p); // T=int*, 返回 int

// 关键:只有被选中的分支会被编译!
// 如果 T=int, *t 甚至不会出现在编译结果中
// 这避免了 SFINAE 中 "有效但完全不用" 的代码编译问题

5.2 SFINAE vs constexpr if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// C++14 SFINAE 写法(冗长)
template <typename T>
typename std::enable_if_t<std::is_pointer_v<T>, std::remove_pointer_t<T>>
getValue_sfinae(T t) { return *t; }

template <typename T>
typename std::enable_if_t<!std::is_pointer_v<T>, T>
getValue_sfinae(T t) { return t; }

// C++17 if constexpr 写法(清晰)
template <typename T>
auto getValue_clean(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t;
} else {
return t;
}
}

5.3 类模板特化的替代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 旧方式:偏特化
template <typename T, typename Enable = void>
struct Container {
void print() const {};
};

template <typename T>
struct Container<T, std::enable_if_t<std::is_integral_v<T>>> {
void print() const { std::cout << "integral\n"; }
};

// C++17 方式:单模板 + if constexpr
template <typename T>
struct ModernContainer {
void print() const {
if constexpr (std::is_integral_v<T>) {
std::cout << "integral: " << value << "\n";
} else {
std::cout << "non-integral\n";
}
}
T value;
};

六、constexpr 的优化意义

编译期计算的直接效果:

1
2
3
4
5
6
7
8
9
10
11
12
constexpr int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}

// 编译期完全展开,运行时直接加载立即数
constexpr int fib20 = fib(20); // 编译器计算出 6765
// 汇编中直接 mov $6765, %eax

int runtime_call(int n) {
return fib(n); // 如果 n 非编译期常量,运行时递归执行
}

零开销抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 编译期哈希:字符串编译期转为整数
constexpr uint32_t hash(const char* str, uint32_t h = 0) {
return str[0] == 0 ? h : hash(str + 1, h * 31 + str[0]);
}

switch (hash("hello")) { // 编译期算出 hash 值
case hash("hello"): break; // case 标签需要编译期常量
case hash("world"): break;
case hash("foobar"): break;
// O(1) 字符串 switch,零运行时开销!
}

// 对比 std::string switch:必须 if-else 链 + 字符串比较,O(n)

内存布局优势:

1
2
3
4
5
6
7
8
9
// constexpr 数据直接进入 .rodata(只读数据段),可能共享
constexpr std::array<int, 1000> lookupTable = generateTable();

// 运行时生成:堆/栈分配,每次调用复制
std::array<int, 1000> generateRuntime() {
std::array<int, 1000> result;
// ... 填充
return result;
}

七、constexpr 与模板元编程的结合

这是 constexpr 最强大的应用场景,将模板元编程从模板特化的繁琐语法中解放出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 原始代码的现代 constexpr 版本

// 基础:编译期布尔值
template <class T, T v>
struct m_integral_constant {
static constexpr T value = v;
using value_type = T;
using type = m_integral_constant;

// C++14: 转换函数也可以是 constexpr
constexpr operator value_type() const noexcept { return value; }
constexpr value_type operator()() const noexcept { return value; }
};

template <bool B>
using m_bool_constant = m_integral_constant<bool, B>;

using m_true_type = m_bool_constant<true>;
using m_false_type = m_bool_constant<false>;

// is_pair: 现代写法对比
namespace classic {
// 传统模板特化
template <class T>
struct is_pair : m_false_type {};

template <class T1, class T2>
struct is_pair<std::pair<T1, T2>> : m_true_type {};
}

namespace modern {
// C++17 if constexpr 版本(更灵活)
template <class T>
struct is_pair : m_false_type {};

template <class T1, class T2>
struct is_pair<std::pair<T1, T2>> : m_true_type {};

// 辅助变量模板
template <class T>
inline constexpr bool is_pair_v = is_pair<T>::value;
}

// C++20 concepts(更现代)
template <class T>
concept Pair = requires(T t) {
typename T::first_type;
typename T::second_type;
t.first;
t.second;
};

C++ constexpr、consteval、constinit
http://example.com/2026/05/22/C++-constexpr-consetval-constinit/
作者
Yu xin
发布于
2026年5月22日
许可协议