CppCon 2025 学习:Constexpr STL Containers: Challenges and a Limitless Allocator Implementation
使用newdelete用于临时计算在同一次 constexpr 求值中完全释放Non-transient constexpr allocation(非瞬态 constexpr 分配)背景左侧:右侧:代码框代码Ð
1⃣ constexpr 的定义
constexpr
- 一个变量或函数的值可以出现在常量表达式中
- 常量表达式(Constant Expression)——可以在编译期求值的表达式
理解:
constexpr表示 编译期常量,编译器可以在编译时计算其值,而不是在运行时- 常量表达式(Constant Expression)就是 在编译期间能够确定结果的表达式
2⃣ 历史发展
| C++ 标准 | constexpr 功能 |
|---|---|
| Before C++11 | 只能进行基础的常量计算,比如整数常量和字面值计算 |
| C++11 | 引入 constexpr 关键字和 constexpr 函数,可以定义编译期函数和常量变量 |
| C++14/17/20 | constexpr 函数可以有更复杂的逻辑,比如分支语句、循环 |
| C++26(预计) | 计划将 所有标准容器(vector, map 等)都支持 constexpr,允许在编译期构造容器 |
3⃣ 示例
3.1 常量变量
constexpr int Square(int x) {
return x * x; // 编译期可求值
}
int main() {
constexpr int Value = Square(5); // Value = 25,编译期确定
int arr[Value]; // 可以用作数组大小
}
- ✓
Value在编译期就已经知道是 25 - ✗ 普通变量
int a = Square(5);只能在运行时求值
3.2 C++14 以后:更复杂的函数
constexpr int Factorial(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
constexpr int fact5 = Factorial(5); // 120,编译期求值
- 以前
constexpr函数只能写一行返回值表达式 - C++14 以后可以使用 循环、分支、局部变量
3.3 与标准容器结合(C++26 预计)
// 预计未来可以这样:
constexpr std::vector<int> vec = {1,2,3,4}; // 在编译期就构造 vector
- 目前还不完全支持
- 目标是 所有标准容器都可用于
constexpr
4⃣ 总结
constexpr用于 在编译期求值的变量或函数- 优点:
- 提前计算,提升运行时性能
- 可用于数组大小、模板参数等需要编译期常量的地方
- 发展趋势:
- C++11 引入基础功能
- C++14/17/20 支持更复杂的编译期计算
- C++26 预计支持标准容器
constexpr
一、constexpr 中的内存分配:总体概念
在 C++20 之后,constexpr 函数中允许使用 new / delete,但有非常严格的限制。
核心思想只有一句话:
编译期分配的内存,不能“逃出”编译期。
二、两个核心分类
1⃣ Transient allocation(瞬时分配)
定义:
- 在
constexpr求值期间分配 - 在同一个
constexpr求值过程中释放 - 不产生任何运行期可观察状态
合法
推荐
这是你图里主要展示的内容
2⃣ Non-transient allocation(非瞬时分配)
定义:
- 在
constexpr中分配 - 没有在同一次常量求值中释放
- 或者 分配结果被用于运行期
✗ 非法
✗ 编译失败
三、第一张图:只有指针,没有分配
constexpr void foo() {
int* p1;
}
理解
- 这里只是声明了一个指针
- ✗ 没有
new - ✗ 没有内存分配
- 编译期只是“知道有个指针变量”
完全合法
指针本身 ≠ 内存分配
四、第二张图:constexpr + new(Transient allocation)
constexpr void foo() {
int* p1;
p1 = new int[5];
}
发生了什么?
- 编译器在 编译期堆(Compile time heap) 中:
- 分配 5 个
int
- 分配 5 个
p1指向这块编译期内存
问题来了:
- 这块内存 没有释放
p1是局部变量,但内存泄漏发生在 编译期
✗ 非法:Non-transient allocation
五、第三张图:new + delete(正确的 Transient allocation)
constexpr void foo() {
int* p1;
p1 = new int[5];
delete[] p1;
}
编译期行为顺序
new int[5]- 在 编译期堆 分配内存
- 使用(即使什么都不做也没关系)
delete[] p1- 在同一次 constexpr 求值中释放
合法
典型的 Transient constexpr allocation
- 在同一次 constexpr 求值中释放
六、为什么必须成对出现?
编译期的“内存守恒规则”
你可以把 constexpr 求值当成一个数学函数:
- 输入:常量
- 输出:常量
- 不允许副作用
可以理解为:
constexpr evaluation ; ≡ ; pure function \text{constexpr evaluation} ;\equiv; \text{pure function} constexpr evaluation;≡;pure function
而: new产生副作用delete抵消副作用
只有:
new + delete = 副作用为 0
编译器才允许。
七、第四、第五张图:为什么这种情况仍然非法?
✗ 内存“逃逸”(escape)
constexpr int* foo() {
int* p = new int(42);
return p; // ✗
}
为什么不行?
- 返回了指向 编译期堆 的指针
- 运行期根本不存在这块内存
违反规则:
constexpr 中分配的内存不能被观察到
八、Compile time heap ≠ Runtime heap
你图中刻意画了两个“heap”,这是非常关键的理解点。
| 类型 | 是否真实存在 |
|---|---|
| Compile time heap | ✗ 只是编译器内部模型 |
| Runtime heap | 程序运行时真实存在 |
编译期堆只是为了“算出结果”,不是程序的一部分
九、总结(直接可背)
✓ constexpr 中允许的事情
- 使用
new/delete - 用于临时计算
- 在同一次 constexpr 求值中完全释放
✗ constexpr 中禁止的事情
- 内存泄漏
- 返回分配的指针
- 把编译期分配的对象用于运行期
- 非 transient allocation
十、一句话版本
constexpr中的内存分配只能是“瞬时的”,就像草稿纸,用完立刻撕掉。
constexpr int* foo() {
int* p1;
p1 = new int[5];
return p1;
}
int main() {
//
constexpr int* p2 = foo();
}
<source>: In function 'int main()':
<source>:3:19: error: 'foo()' is not a constant expression because it refers to a result of 'operator new'
3 | p1 = new int[5];
| ^
Compiler returned: 1
https://godbolt.org/z/Pa7ssK9KK
consteval void foo() {
int* p1;
p1 = new int[5];
delete[] p1;
}
int main() {
foo(); // 强制编译期执行
}
https://godbolt.org/z/ExqTvc5es
一、什么是 Non-transient constexpr allocation(非瞬态 constexpr 分配)
先给一句标准语义层面的定义:
Non-transient constexpr allocation
指的是:
在 常量求值(constant evaluation) 期间分配的动态对象,
在 常量求值结束后仍然“可达(reachable)”,
并且被用于构造一个常量表达式的最终结果。
用公式化语言表示:
Allocated object ; ∈ ; constexpr heap \text{Allocated object} ;\in; \text{constexpr heap} Allocated object;∈;constexpr heap
并且:
∃ constexpr result ; R , R → allocated object \exists\ \text{constexpr result}; R,\quad R \rightarrow \text{allocated object} ∃ constexpr result;R,R→allocated object
一旦满足这个条件 → 程序不再是常量表达式。
二、你的代码逐行发生了什么(加注释)
constexpr int* foo() {
// ① 在 constexpr 求值期间调用 operator new[]
return new int[5];
}
int main() {
// ② 这里要求 foo() 在【编译期】被求值
constexpr int* p1 = foo();
}
关键点拆解
| 步骤 | 发生时间 | 行为 |
|---|---|---|
| ① | 编译期 | 在 constexpr 求值中分配一块“编译期堆内存” |
| ② | 编译期 | 试图把这个指针作为常量值保存 |
| ✗ | ✗ | 指针 逃逸(escape) 出 constexpr 求值 |
三、结合你给的图:编译期 vs 编译期堆
你图里其实画得非常标准,我帮你用语言对应一下:
左边蓝色框:Compile time evaluation
- 执行
foo()的 constexpr 语义 - 允许:
- 普通算术
- 局部对象
- 瞬态 new / delete
右边橙色框:Compile time heap
new int[5]分配出的对象- 只能在 constexpr 沙盒内存在
红线(图中的虚线)
constexpr 求值边界
任何对象 / 指针 / 引用都不能跨过这条线
你现在的代码正是:
Compile time heap
↓
constexpr int* p1 ✗
四、为什么 constexpr int* p1 = foo(); 一定非法?
标准的核心规则(非原文,但等价)
在常量表达式求值结束时:
不能存在指向 constexpr 分配对象的指针或引用
也可以理解成:
constexpr result ≠ pointer into constexpr heap \text{constexpr result} \neq \text{pointer into constexpr heap} constexpr result=pointer into constexpr heap
为什么标准要这样设计?
如果允许:
constexpr int* p1 = foo();
编译器就必须回答这些问题:
- 这块内存放哪?
.rodata?.data?- runtime heap?
- 生命周期是谁管理?
- 谁来 delete?
- 多个单元是否共享?
- 指针值是否稳定?
这些问题一旦允许 → constexpr 不再是“纯值系统”
五、为什么“瞬态 constexpr 分配”是允许的?
对比这个合法代码:
consteval void foo() {
int* p = new int[5]; // constexpr heap
p[0] = 42; // 使用
delete[] p; // 释放
}
这里满足:
- 分配
- 使用
- 释放
- 没有任何指针逃逸
用集合表示:
Live constexpr heap objects at end = ∅ \text{Live constexpr heap objects at end} = \varnothing Live constexpr heap objects at end=∅
这就是 Transient allocation(瞬态分配)
六、Non-transient vs Transient(对照表)
| 类型 | 是否允许 | 原因 |
|---|---|---|
| new + delete 在 constexpr 内 | ✓ | 不逃逸 |
| 返回 new 得到的指针 | ✗ | 指针逃逸 |
| constexpr 保存指针 | ✗ | 非纯值 |
constexpr 返回 std::array |
✓ | 值语义 |
| constexpr 返回结构体 | ✓ | 聚合可嵌入 |
七、正确的 constexpr“数组”写法(替代方案)
✓ 方案 1:std::array
#include <array>
constexpr std::array<int, 5> foo() {
return {1, 2, 3, 4, 5};
}
int main() {
constexpr auto arr = foo(); // OK
}
✓ 方案 2:自定义聚合
struct Buffer {
int data[5];
};
constexpr Buffer foo() {
return {};
}
✗ 永远不允许(constexpr 结果中)
constexpr int*;
constexpr int&;
constexpr std::unique_ptr<int>;
八、一句话终极总结(建议背下来)
constexpr 允许动态分配,
但不允许把分配得到的地址作为常量结果的一部分。
或者更精炼:
constexpr heap 只能在 constexpr 沙盒里存在,不能跨越边界。
一、先给结论总览(帮助你建立整体模型)
C++20 中:
- ✓ 允许:在
constexpr求值过程中临时进行动态分配,并在求值结束前完全释放- ✗ 禁止:任何 动态分配结果“逃逸”到常量表达式结果中
用一个集合关系表达:
constexpr evaluation结束时 ; ⇒ ; constexpr heap 必须为空 \text{constexpr evaluation结束时} ;\Rightarrow; \text{constexpr heap 必须为空} constexpr evaluation结束时;⇒;constexpr heap 必须为空
二、什么是 Transient constexpr allocation(瞬态)
你的代码(加注释版)
constexpr void foo() {
// ① constexpr 求值期间,创建一个 vector
// C++20 起,vector 的构造函数是 constexpr 的
std::vector<int> v = { 1, 2, 3 };
// ② 手动使用 allocator 分配内存(constexpr 允许)
auto* p = std::allocator<int>().allocate(3);
// ③ 使用完立即释放
std::allocator<int>().deallocate(p, 3);
}
为什么这是 合法的
关键点在于:
v是 局部对象p是 局部指针- 所有动态分配:
- 在 constexpr 求值期间发生
- 在 constexpr 求值结束前释放
也就是说:
allocated objects lifetime ⊂ constexpr evaluation \text{allocated objects lifetime} \subset \text{constexpr evaluation} allocated objects lifetime⊂constexpr evaluation
没有任何东西跨出 constexpr 求值边界
三、什么是 Non-transient constexpr allocation(非瞬态)
1⃣ 返回 std::vector(看似合理,实际非法)
constexpr std::vector<int> foo() {
return { 1, 2, 3 };
}
乍一看:
- 没有
new - 没有指针
- 看起来“值语义”
✗ 但这是 非瞬态
为什么?
因为:
std::vector内部必然进行动态分配- 返回的
vector对象:- 持有指向 constexpr heap 的指针
- 生命周期超出了 constexpr 求值
也就是:
constexpr result ; → ; constexpr heap \text{constexpr result} ;\rightarrow; \text{constexpr heap} constexpr result;→;constexpr heap
这正是 Non-transient allocation
四、Top-level constexpr 对象为什么全部报错?
✗ 情况 1:返回的 vector 用于 constexpr 对象
constexpr std::vector<int> v1 = foo(); // ✗ 编译期错误
问题不在 foo(),而在 这里:
top-level constexpr 对象必须完全由“纯值”构成
而std::vector内部包含:
pointer + size + capacity
其中:
pointer指向动态分配内存- 这块内存无法成为编译期常量
✗ 情况 2:直接 constexpr 初始化 vector
constexpr std::vector<int> v2 = { 1, 2, 3, 4, 5, 6 }; // ✗
即使不经过函数:
- vector 构造
- 仍然需要动态分配
- 仍然产生非瞬态分配
✗ 情况 3:constexpr 指针指向 allocator 分配内存
constexpr int* data = std::allocator<int>().allocate(3); // ✗
这是最直观的非法例子:
data是一个 constexpr 结果- 它指向动态分配内存
违反规则:
constexpr pointer ; ↛ ; dynamic storage \text{constexpr pointer} ;\nrightarrow; \text{dynamic storage} constexpr pointer;↛;dynamic storage
五、用一张“生死线”来理解
┌────────────────────┐
│ constexpr evaluation│
│ │
│ new / allocate │
│ use │
│ delete / free │
│ │
└─────────┬──────────┘
│ ✗ 不可跨越
▼
constexpr object / value
只要有一根指针 / 引用 / 容器成员跨过去 → 非法
六、为什么标准要这样设计?(核心原因)
1⃣ constexpr 的本质是“值系统”
constexpr ≠ “提前执行的 runtime”
而是:
constexpr = pure, immutable, relocatable value \text{constexpr} = \text{pure, immutable, relocatable value} constexpr=pure, immutable, relocatable value
2⃣ 动态内存违反的三个点
| 问题 | 解释 |
|---|---|
| 地址不稳定 | 编译器无法保证唯一地址 |
| 生命周期模糊 | 谁释放?什么时候? |
| 链接不安全 | 多 TU / ODR 问题 |
七、C++20 vs C++23 / C++26(重要趋势)
| 标准 | 情况 |
|---|---|
| C++20 | vector constexpr 构造,但不能作为 constexpr 对象 |
| C++23 | constexpr 使用范围扩大,但仍禁止 non-transient |
| C++26(提案中) | 可能允许更多容器 constexpr 化 |
但即便 C++26:
指针逃逸几乎不可能被允许
八、正确的 constexpr 替代方案(总结)
✓ 用值语义容器
constexpr std::array<int, 3> a = {1, 2, 3};
✓ 用聚合 + 固定大小
struct Data {
int x[3];
};
constexpr Data d{};
✗ 不要做的事
constexpr std::vector<int>
constexpr std::string
constexpr int*
constexpr unique_ptr
九、一句话终极总结(可当面试答案)
C++20 允许 constexpr 中“临时”动态分配,
但任何动态分配结果都不能成为 constexpr 对象的一部分。
一、C++20 已通过提案(Accepted Proposals for C++20)
1⃣ P0784 — constexpr new/delete + constexpr 析构函数
提案内容
允许在
constexpr求值中:
- 使用
new / delete- 调用析构函数
- 通过
std::allocator分配(仅 transient)
核心限制(非常重要)
只允许 transient allocation(瞬态分配)
也就是:
allocation lifetime ⊆ constexpr evaluation \text{allocation lifetime} \subseteq \text{constexpr evaluation} allocation lifetime⊆constexpr evaluation
合法示例
constexpr int foo() {
int* p = new int(42); // constexpr 中允许
int v = *p;
delete p; // 必须释放
return v;
}
非法示例(非瞬态)
constexpr int* foo() {
return new int(42); // ✗ 指针逃逸
}
2⃣ constexpr placement new(P1004 → std::construct_at)
为什么要这个?
传统 placement new:
new (ptr) T(args...);
不是 constexpr
C++20 解决方案
#include <memory>
constexpr void foo() {
alignas(int) unsigned char buf[sizeof(int)];
int* p = std::construct_at(reinterpret_cast<int*>(buf), 42);
std::destroy_at(p);
}
关键点
- 不涉及 heap
- 生命周期完全受控
- 非常适合 constexpr 构造复杂对象
3⃣ constexpr support for std::vector
常见误解澄清
✗ 不是“vector 可以是 constexpr 对象”
而是:
✓ vector 的成员函数 / 构造 / 析构可以在 constexpr 中执行
合法(transient)
constexpr int sum() {
std::vector<int> v = {1, 2, 3};
return v[0] + v[1] + v[2];
}
非法(non-transient)
constexpr std::vector<int> v = {1, 2, 3}; // ✗
4⃣ P0980 — constexpr std::string
规则与 std::vector 完全一致
constexpr int len() {
std::string s = "hello";
return s.size();
}
✗ 仍然不能:
constexpr std::string s = "hello"; // C++20 ✗
5⃣ P1002 — 允许 try / catch 于 constexpr
C++20 之前
constexpr int foo() {
try { return 1; } catch (...) {} // ✗
}
C++20
constexpr int foo() {
try {
return 1;
} catch (...) {
return 0;
}
}
但异常必须在 constexpr 中被捕获
6⃣ constexpr std::pointer_traits
支持在 constexpr 中操作指针 traits(工具性增强)
二、C++23:迈出“非瞬态”的第一步
P2273 — constexpr std::unique_ptr(C++23)
表面上看是非瞬态?
constexpr auto p = std::make_unique<int>(42);
实际规则(非常巧妙)
unique_ptr 允许 constexpr
但其管理的对象仍然必须在 constexpr 结束前销毁
本质
- 这是 “语法层面”支持
- 不是真正的 non-transient heap
三、C++26 已接受提案(非常前沿)
1⃣ P3372 — constexpr containers and adaptors
扩展 constexpr 能力
但仍然不等价于“constexpr heap 持久化”
2⃣ P2747 — constexpr placement new
更底层、更灵活,减少库 hack
3⃣ P2738 / P2686 — constexpr cast / structured bindings
- 让 constexpr 更“像运行期”
- 不改变内存模型本质
4⃣ P3491 — define_static_{string,object,array}
非常重要的新机制(C++26)
constexpr auto s =
define_static_string("hello");
特点
- 不是 heap
- 编译器生成静态只读对象
- 地址稳定
- 生命周期 = 程序生命周期
局限性
| 限制 | 原因 |
|---|---|
| 仅标准容器 | 编译器内建支持 |
| 不支持自定义容器 | 无法推断内存布局 |
| 每种类型需要单独 builtin | 工程复杂 |
四、进行中的提案(Work-in-Progress)
1⃣ P3094 — std::basic_fixed_string
目标:
让 string 变成 值语义、无 heap
constexpr basic_fixed_string<32> s = "hello";
2⃣ P3032 — Less transient constexpr allocation
放宽“必须立即释放”的限制
但 仍未允许自由 non-transient heap
3⃣ P1974 / P2670 — 真·Non-transient constexpr allocation
震撼示例(提案中)
constexpr std::unique_ptr<std::unique_ptr<int>> uui =
make_unique<std::unique_ptr<int>>(
make_unique<int>(42)
);
int main() {
std::unique_ptr<int>& ui = *uui;
ui.reset();
}
为什么极其危险?
- constexpr heap 变成“全局状态”
- ODR、链接、ABI 问题巨大
- 编译器复杂度暴涨
目前仍未通过
五、被拒绝提案(Rejected Proposals)
1⃣ P0784(扩展部分)— 非瞬态分配 ✗
瞬态部分通过,非瞬态部分被拒
原因总结:
constexpr heap persistence ⇒ ODR + ABI 灾难 \text{constexpr heap persistence} \Rightarrow \text{ODR + ABI 灾难} constexpr heap persistence⇒ODR + ABI 灾难
2⃣ P0574 — std::constexpr_vector
企图:
constexpr_vector<int> v = {1,2,3};
✗ 被拒
原因:
- 改变 vector 安全模型
- 引入“伪堆”
- 攻击面巨大
3⃣ P0639 — constexpr_allocator(固定容量)
template <class T, unsigned N = 100>
struct constexpr_allocator {
T data[N] = {};
};
问题
| 问题 | 解释 |
|---|---|
| 固定容量 | 无法泛化 |
| 内存浪费 | 不可控 |
| ABI 不稳定 | 编译器难支持 |
六、最终总结(一句话版)
C++20 打开了 constexpr 的“执行能力”,
但始终没有打开 constexpr 的“持久堆”。
用公式总结整条路线:
constexpr evolution = execution power ↑ + memory persistence ; almost unchanged \text{constexpr evolution} = \text{execution power} \uparrow + \text{memory persistence} ;\text{almost unchanged} constexpr evolution=execution power↑+memory persistence;almost unchanged
一、什么是「Containers with Local Buffer」
1⃣ 定义(直观版)
Local Buffer 容器指的是:
容器所需的全部存储空间
直接嵌在对象自身内部,
不依赖堆(heap)分配
数学化描述:
storage ( C ) ⊆ object ( C ) \text{storage}(C) \subseteq \text{object}(C) storage(C)⊆object(C)
2⃣ 为什么 Local Buffer 对 constexpr 至关重要?
因为:
C++20 及之前:
constexpr不允许非瞬态堆分配
即:
constexpr object lifetime ⊉ heap lifetime \text{constexpr object lifetime} \not\supseteq \text{heap lifetime} constexpr object lifetime⊇heap lifetime
所以,只有“不用 heap 的容器”才能成为 top-levelconstexpr对象。
二、哪些容器是「constexpr 容器」
✓ 真·constexpr 容器(标准层面)
std::array
constexpr std::array<int, 3> a = {1, 2, 3};
为什么 std::array 可以?
- 没有 heap
- 内部就是一个
T[N] - 生命周期 = 对象生命周期
template<class T, size_t N>
struct array {
T elems[N]; // 本地缓冲区
};
完全满足 constexpr 的对象模型
误解区:std::string
你看到的现象
static constexpr std::string s1 = "string for example"; // ✗
static constexpr std::string s2 = "small"; // ✓(某些实现)
真相:这是 SSO 假象
三、std::string + constexpr 的真相(重点)
1⃣ SSO 是什么?
SSO(Small String Optimization):
小字符串直接存储在
std::string对象内部
大字符串才使用 heap
实现示意:
struct string {
union {
char small[15]; // SSO buffer
char* heap_ptr;
};
};
2⃣ 为什么这是“假 constexpr 支持”?
表面现象
static constexpr std::string s2 = "small"; // 编译通过
但标准视角:
标准并不保证 SSO 的存在
换句话说:
SSO ∉ C++ standard \text{SSO} \notin \text{C++ standard} SSO∈/C++ standard
3⃣ 标准的结论
static constexpr std::string s = "...";
✗ 在标准意义上是非法的
即使:
- 某些编译器
- 某些 STL 实现
- 某些优化级别
能“侥幸通过”
极其重要的结论
不能因为“SSO 恰好没分配堆”就认为这是 constexpr 合法的
这是 实现细节泄露,不是语言保证。
四、为什么 Boost / userver 的容器不算 constexpr
1⃣ static_vector / small_vector
这些容器的设计:
- 有本地 buffer
- 容量超过就 fallback 到 heap
示例:
boost::container::static_vector<int, 16> v;
为什么不能 constexpr?
- 构造函数不是
constexpr - 内部逻辑包含条件 heap 分配
- 标准无法保证:
∀ T , ∀ i n p u t , no heap allocation \forall T, \forall input, \text{no heap allocation} ∀T,∀input,no heap allocation
2⃣ FastPimpl(userver)
FastPimpl 本质是:
在对象内部预留一块 byte buffer,用来 placement new
为什么也不行?
- placement new 直到 C++20 才 constexpr
- 生命周期管理复杂
- 析构逻辑不满足 constexpr 要求
五、标准库中「真正符合条件」的只有谁?
✓ 结论
In the standard library, only
std::arrayqualifies
原因总结:
| 条件 | std::array | std::string | vector |
|---|---|---|---|
| 无 heap | ✓ | ✗ | ✗ |
| 存储在对象内部 | ✓ | ✗ | ✗ |
| 标准保证 | ✓ | ✗(SSO 非标准) | ✗ |
| top-level constexpr | ✓ | ✗ | ✗ |
六、总结(Overview: Conclusion 解读)
幻灯片三条结论逐条解释
✓ 1. constexpr allocations
Currently, only transient allocations are supported.
heap alloc ⇒ must be destroyed within constexpr eval \text{heap alloc} \Rightarrow \text{must be destroyed within constexpr eval} heap alloc⇒must be destroyed within constexpr eval
✓ 2. top-level constexpr containers
Containers with local buffer are supported.
前提是:
no heap ∧ standard guaranteed \text{no heap} \land \text{standard guaranteed} no heap∧standard guaranteed
✓ 3. In the standard library, only std::array qualifies
因为:
std::string的 local buffer(SSO)不在标准里vector永远需要 heap- 其他容器都无法保证 constexpr 对象模型
七、一个一句话终极总结
constexpr 不是“有没有分配堆”,
而是“标准是否保证你永远不会分配堆”。
一、什么是「Non-Trivial Buffer Size」
1⃣ 核心含义
**Non-Trivial Buffer Size(非平凡缓冲区大小)**指的是:
在构造对象时,所需的内部存储大小
不能在编译期、也不能在构造函数入口处一次性确定
形式化描述:
required_storage ≠ known at construction start \text{required\_storage} \neq \text{known at construction start} required_storage=known at construction start
2⃣ 为什么 initializer_list 是“罪魁祸首”
std::initializer_list<T> 的本质是:
struct initializer_list {
const T* begin;
size_t size;
};
它只告诉你 size,但:
- 不告诉你:
- 最终 capacity
- 内部扩容策略
- 是否需要 rehash(对 hash 容器)
二、Example 1:std::vector + initializer_list
1⃣ 代码
std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
2⃣ 图的含义逐条解释
(1)上方蓝色数组:initializer_list
{1,2,3,4,5,6}- size = 6
- 是一个临时只读数组
(2)下方黄色框:std::vector 对象
struct vector {
T* data; // 指向堆内存
size_t size; // 当前元素数
size_t capacity;// 分配的容量
};
(3)关键点:size ≠ capacity
图中明确写了:
size = 6 != 8 (capacity)
原因:
- vector 不保证 capacity == size
- 实现通常会:
- 直接分配 >= size 的内存
- 甚至向上取整 / 预留增长空间
(4)这就是「Non-Trivial Buffer Size」
即使:
initializer_list.size() == 6
但:
capacity ≥ size \text{capacity} \ge \text{size} capacity≥size
且:
capacity = f ( implementation ) \text{capacity} = f(\text{implementation}) capacity=f(implementation)
3⃣ 标准接口(C++20 前后)
// C++11 起
vector(std::initializer_list<T> init,
const Allocator& alloc = Allocator());
// C++20 起
constexpr vector(std::initializer_list<T> init,
const Allocator& alloc = Allocator());
注意重点
- constexpr ≠ 没有堆分配
- C++20 只允许 transient allocation
- capacity 仍然是实现相关的
4⃣ 复杂度
构造复杂度
O ( N ) O(N) O(N)
其中:
- N = init.size() N = \text{init.size()} N=init.size()
但: - 是否一次分配?
- 是否有额外拷贝?
实现决定
三、Example 2:std::unordered_map + initializer_list
1⃣ 代码
std::unordered_map<int, int> m = {
{1, 1},
{2, 3},
{3, 3}
};
2⃣ 为什么 unordered_map 更“非平凡”
unordered_map 的内部结构
- bucket array(桶数组)
- 每个 bucket 是链表 / 红黑树(实现相关)
3⃣ 标准构造函数
unordered_map(
std::initializer_list<value_type> init,
size_type bucket_count = /* implementation-defined */,
const Hash& hash = Hash(),
const key_equal& equal = key_equal(),
const Allocator& alloc = Allocator()
);
关键问题
bucket_count:- 默认值是 implementation-defined
- 是否提前 rehash?
- 负载因子是多少?
4⃣ 标准规定的复杂度
官方复杂度说明
Average case: O ( N ) O(N) O(N)
Worst case: O ( N 2 ) O(N^2) O(N2)
其中:
N = init.size() N = \text{init.size()} N=init.size()
5⃣ 为什么最坏是 O ( N 2 ) O(N^2) O(N2)
标准等价实现(你给的伪代码)
unordered_map(initializer_list<value_type> il) {
insert(il.begin(), il.end());
}
void insert(InputIt first, InputIt last) {
for (; first != last; ++first)
table_.insert_unique(*first);
}
问题出在哪里?
- 每次
insert:- 可能触发 rehash
- rehash 是 O ( N ) O(N) O(N)
- 如果每插一次都 rehash:
1 + 2 + 3 + ⋯ + N = O ( N 2 ) 1 + 2 + 3 + \dots + N = O(N^2) 1+2+3+⋯+N=O(N2)
6⃣ vector vs unordered_map 对比
| 容器 | Buffer Size 是否可预测 | 复杂度稳定性 |
|---|---|---|
| vector | 相对可预测 | O ( N ) O(N) O(N) |
| unordered_map | 不可预测 | 平均 O ( N ) O(N) O(N),最坏 O ( N 2 ) O(N^2) O(N2) |
四、为什么这对 constexpr 是灾难性的
1⃣ constexpr 要求什么?
- 编译期执行
- 资源使用必须:
- 有上界
- 可推导
- 无不可控 rehash / growth
2⃣ Non-Trivial Buffer Size 的本质问题
allocation count ≰ compile-time known bound \text{allocation count} \not\le \text{compile-time known bound} allocation count≤compile-time known bound
对 vector:
- capacity 策略不标准化
- allocator 行为实现相关
对 unordered_map:
- bucket 数
- rehash 策略
- hash 分布
全部不可 constexpr 推导
五、一句话总结
initializer_list 构造的容器,其内部缓冲区大小在标准层面是“不可预测的”,
因此是 Non-Trivial Buffer Size。
进一步推论:
- ✗ 不适合作为 top-level
constexpr对象 - ✗ 无法保证 constexpr 分配次数
- ✗ 复杂度在 constexpr 环境不可控
六、终极总结
std::initializer_list并不携带“容量语义”,
任何基于它构造的动态容器,其内存模型在标准层面都是非平凡的。
一、从源码出发:initializer_list 构造到底做了什么?
1⃣ 你给出的核心实现(等价于 libc++ / libstdc++ 思路)
unordered_map(initializer_list<value_type> __il)
{
insert(__il.begin(), __il.end());
}
void insert(_InputIterator __first, _InputIterator __last)
{
for (; __first != __last; ++__first)
__table_.__insert_unique(*__first);
}
逐行解释(加注释)
// 使用 initializer_list 构造 unordered_map
unordered_map(std::initializer_list<value_type> il)
{
// ✗ 没有提前分配 bucket / node
// ✗ 没有 reserve
// 只是逐个插入
insert(il.begin(), il.end());
}
template<class InputIterator>
void insert(InputIterator first, InputIterator last)
{
// 对 initializer_list 中的每一个元素
for (; first != last; ++first)
{
// 单个元素插入
// 可能触发:
// - bucket 扩容
// - rehash
// - node / pointer 多次分配
table_.insert_unique(*first);
}
}
结论一句话:
unordered_map 的 initializer_list 构造 = N 次普通 insert
二、第一张图:多次(Transient Allocations)
(左半边代码 + 右半边 allocator / heap)
1⃣ 图左:伪代码含义
container(init_type init)
{
data1 = a.allocate(1);
data2 = a.allocate(2);
data3 = a.allocate(4);
a.deallocate(data1, 1);
}
这表示什么?
- 容器构造过程中:
- 分配 1 个单位
- 再分配 2 个单位
- 再分配 4 个单位
- 早期分配很快就被释放
多次(transient)分配
2⃣ 对应 unordered_map 的真实情况
每一次 __insert_unique 可能触发:
| 分配类型 | 用途 |
|---|---|
| node allocator | 分配 pair<const K, V> 节点 |
| pointer allocator | bucket / next 指针 |
| rehash | 重新分配 bucket array |
3⃣ 图中「X」的含义
- 红色 ✗:
- 表示中间分配被废弃
- 数据迁移后被 deallocate
- 只有最后一次分配存活
4⃣ 为什么这叫 Non-Trivial Buffer Size?
因为:
Total allocations ≠ f ( N ) \text{Total allocations} \neq f(N) Total allocations=f(N)
而是:
allocations = f ( N , hash , load_factor , impl ) \text{allocations} = f(N, \text{hash}, \text{load\_factor}, \text{impl}) allocations=f(N,hash,load_factor,impl)
三、第二张图:unordered_map 的真实内存模型
(pair allocator / node allocator / pointer allocator)
1⃣ unordered_map 不是「一块内存」
内部至少包含三种资源
| 名称 | 图中位置 | 作用 |
|---|---|---|
| pair allocator | 左上 | 构造临时 pair |
| node allocator | 右上 | 分配链表节点 |
| pointer allocator | 右上 | bucket / next 指针 |
| heap | 下方 | 实际内存来源 |
2⃣ 一个 initializer_list 元素变成什么?
从图中可总结为:
{ key, value }
↓
node {
pair<key, value>
next pointer
}
即:
1 pair ⇒ 1 node + 1 pointer \text{1 pair} \Rightarrow \text{1 node} + \text{1 pointer} 1 pair⇒1 node+1 pointer
3⃣ 图中公式的真实含义
你图里这部分:
pairs => nodes + pointers
等价于:
N pairs ⇒ N nodes + N pointers N\ \text{pairs} \Rightarrow N\ \text{nodes} + N\ \text{pointers} N pairs⇒N nodes+N pointers
但这只是最低下界。
4⃣ 真正的问题:bucket 数是未知的
- bucket 数:
- 实现定义
- 可能 > N
- rehash 次数:
- 不固定
- 每次 rehash:
- 重分配 bucket
- 移动所有节点
四、复杂度为什么是 Average O ( N ) O(N) O(N),Worst O ( N 2 ) O(N^2) O(N2)?
1⃣ 平均情况(理想)
- 一次性分配 bucket
- 插入不触发 rehash
T ( N ) = O ( N ) T(N) = O(N) T(N)=O(N)
2⃣ 最坏情况(图中真实暗示的路径)
假设每插一次都 rehash:
T ( N ) = 1 + 2 + 3 + ⋯ + N T(N) = 1 + 2 + 3 + \dots + N T(N)=1+2+3+⋯+N
T ( N ) = O ( N 2 ) T(N) = O(N^2) T(N)=O(N2)
3⃣ 图中「intermediate = 7」的含义
- initializer_list 有 6 个元素
- 中间可能经历:
- 第 7 次额外分配(rehash)
- 这一步在构造开始时是不可预测的
五、为什么 constexpr 无法接受这种模型?
1⃣ constexpr 的硬性要求
- 分配次数必须:
- 有上界
- 可在编译期推导
- 不允许:
- 无限或不确定 rehash
- 实现依赖行为
2⃣ unordered_map initializer_list 构造的问题总结
| 问题 | 是否 constexpr 友好 |
|---|---|
| bucket_count 实现定义 | ✗ |
| rehash 次数不可知 | ✗ |
| node / pointer 多次分配 | ✗ |
| 最坏 O ( N 2 ) O(N^2) O(N2) | ✗ |
3⃣ 本质失败点(一句话)
Memory footprint ⊈ compile-time bound \text{Memory footprint} \not\subseteq \text{compile-time bound} Memory footprint⊆compile-time bound
六、最终总结(高度凝练)
std::unordered_map的initializer_list构造
是一个「语义上简单、实现上极度复杂」的接口。
它的问题不在 API,而在于:
- 没有容量语义
- 没有 allocation 上界
- 必须逐个 insert
- rehash 行为完全不可预测
一句话终结版
Initializer-list construction of
unordered_mapimplies a sequence of inserts,
leading to non-trivial, unbounded, and implementation-defined buffer growth.
一、结论的核心问题
Where to store container’s elements?
容器的元素到底存放在哪里?
这是所有 constexpr / 高性能容器的根本问题。
常见存储位置只有两种
| 存储方式 | 特点 |
|---|---|
| 堆(heap) | 大、灵活,但需要动态分配 |
| 本地缓冲区(local buffer) | 固定、可控、适合 constexpr |
constexpr 的前提条件
constexpr 环境中:
- ✗ 不能依赖不可预测的动态内存
- 只能使用大小在编译期可界定的存储
初步结论
如果要 constexpr / 编译期构造容器,
那么元素必须放在「本地 buffer」中。
二、本地 buffer
Where to store container’s elements?
In a local buffer
什么是 local buffer?
template <typename T, std::size_t N>
struct static_vector {
T buffer[N]; // 本地缓冲区
std::size_t size = 0;
};
特点
- 内存连续
- 无
new / delete - 生命周期与对象一致
- constexpr 友好
标准库中谁符合?
| 容器 | 是否本地 buffer |
|---|---|
std::array<T, N> |
✓ 完全 |
std::string |
仅 SSO |
std::vector |
✗ 堆 |
std::unordered_map |
✗ 堆 + node |
到这里为止,一切都很美好
三、真正的难点出现
Determining buffer size is NOT trivial
在一般情况下,确定所需 buffer 的大小并不简单。
为什么“不平凡(non-trivial)”?
对比两个例子
✓ 简单情况:std::array
std::array<int, 4> a;
- 元素数 = 4
- 每个元素大小固定
- buffer 大小:
buffer_size = 4 × s i z e o f ( i n t ) \text{buffer\_size} = 4 \times sizeof(int) buffer_size=4×sizeof(int)
完全可计算
✗ 一般容器(如 unordered_map)
std::unordered_map<int, int> m = {
{1,1}, {2,2}, {3,3}
};
你至少需要:
buffer = nodes + pointers + buckets + rehash overhead \text{buffer} = \text{nodes} + \text{pointers} + \text{buckets} + \text{rehash overhead} buffer=nodes+pointers+buckets+rehash overhead
但问题是:
- bucket 数量?
- rehash 次数?
- load factor?
- 实现细节?
无法在一般情况下给出确定上界
抽象化后的问题
给定构造参数 P P P,求一个 buffer 大小 B B B,使得:
∀ 合法实现 : container ( P ) ⊆ B \forall\ \text{合法实现}: \text{container}(P) \subseteq B ∀ 合法实现:container(P)⊆B
这是一个不可解的泛化问题。
四、那怎么办?
How to define buffer size automatically?
如果不能“一步到位”,就只能:
把“确定大小”和“构造对象”分离。
关键思想
✗ 错误模型(一步构造)
constexpr container c = make_container(args);
- 你还不知道需要多少内存
- 却已经要开始构造
✓ 正确模型(两步法)
Create the object in two steps
五、两步构造模型
Step 1:确定 buffer size
只做分析,不做分配
constexpr std::size_t required_buffer_size =
container_required_buffer(args);
- 不构造真实对象
- 不分配内存
- 只计算需求
Step 2:使用该 size 构造对象
alignas(container) std::byte buffer[required_buffer_size];
constexpr container c(buffer, required_buffer_size, args);
完整示意代码(带注释)
// Step 1: 只计算需要多少 buffer
constexpr std::size_t buf_size =
compute_required_size(init_list);
// Step 2: 提供足够大的本地 buffer
alignas(MyContainer) std::byte buffer[buf_size];
// Step 3: 在 buffer 上构造对象(placement new)
constexpr MyContainer c(buffer, buf_size, init_list);
数学抽象
将构造函数拆解为两个函数:
size = f ( args ) \text{size} = f(\text{args}) size=f(args)
object = g ( args , size ) \text{object} = g(\text{args}, \text{size}) object=g(args,size)
六、最终总结合成一句话
constexpr 容器的唯一可行路径是:
- 使用本地 buffer
- 明确 buffer 上界
- 将“分析内存需求”与“对象构造”分离
但现实中的残酷事实
std::unordered_map:✗ 无法给出 f ( args ) f(\text{args}) f(args)std::vector:✗ 增长策略实现定义std::string: 仅 SSO 情况可行std::array:✓ 完全可行
一句话终极结论
If buffer size cannot be determined before construction,
constexpr construction is fundamentally impossible.
一、这张图整体在解决什么问题?
一句话概括
把“容器构造”拆成多个阶段,每个阶段只做一件事:
- 阶段 1:只数数(不分配)
- 阶段 2:只计算结构(仍不分配)
- 阶段 3:在已知大小的本地 buffer 中真正构造对象
图的三列含义
| 区域 | 颜色 | 含义 |
|---|---|---|
| 左 | 灰色 | Stage 1:计数阶段(counter allocator) |
| 中 | 绿色 | Stage 2:结构阶段(transcript allocator) |
| 右 | 紫色 | Stage 3:实际构造(script allocator + buffer) |
二、Stage 1:Counter Allocator(只数,不存)
对应图中「1」
std::vector<int> v = {1, 2, 3};
做了什么?
- 只遍历 initializer
- 不分配内存
- 不构造元素
- 只统计需要多少「逻辑对象」
图中含义
initializer→{1,2,3}counter allocator- 得到:
n = 3;
抽象模型
struct CounterAllocator {
std::size_t count = 0;
void allocate(std::size_t n) {
count += n; // 只计数
}
};
数学表达
第一阶段计算元素数量
n = ∣ i n i t i a l i z e r ∣ n = |initializer| n=∣initializer∣
关键点
- ✗ 不允许 heap
- ✗ 不允许 placement new
- constexpr 可行
三、Stage 2:Transcript Allocator(只记录,不分配)
对应图中「2」
核心思想
把“将来会发生的内存行为”记录成一份“脚本(transcript)”
做了什么?
- 容器逻辑正常运行
- 每次「想分配内存」时:
- ✗ 不真的分配
- 记录:大小、对齐、顺序
图中表现
- container
- allocator 内部有:
tablen
- data1 / data2 / data3 被打叉
- 虚线箭头 → heap(但没真的发生)
抽象代码
struct AllocationRecord {
std::size_t size;
std::size_t align;
};
struct TranscriptAllocator {
std::vector<AllocationRecord> log;
void* allocate(std::size_t size, std::size_t align) {
log.push_back({size, align});
return fake_address(); // 占位
}
};
输出是什么?
- 一张 “内存使用时间线”
- 可计算:
S = ∑ i align_up ( s i z e i , a l i g n i ) S = \sum_i \text{align\_up}(size_i, align_i) S=i∑align_up(sizei,aligni)
这一阶段的价值
- 得到 确切 buffer 大小
- 得到 对象布局顺序
- ✗ 仍然没有真实对象
四、Stage 3:Script Allocator + Local Buffer(真正构造)
对应图中「3」
输入
来自 Stage 2:
table(分配顺序)n- 总 buffer 大小 s s s
先准备本地 buffer
alignas(std::max_align_t)
std::byte buffer[s];
Script Allocator 的职责
严格按 Stage 2 记录的顺序分配
抽象代码
struct ScriptAllocator {
std::byte* base;
std::size_t offset = 0;
void* allocate(std::size_t size, std::size_t align) {
offset = align_up(offset, align);
void* ptr = base + offset;
offset += size;
return ptr;
}
};
数学形式
p 0 = 0 p_0 = 0 p0=0
p i + 1 = align_up ( p i , a i ) + s i p_{i+1} = \text{align\_up}(p_i, a_i) + s_i pi+1=align_up(pi,ai)+si
最终:
p k ≤ s p_k \le s pk≤s
此时发生了什么?
- 真正构造 data1 / data2 / data3
- 所有对象都位于 本地 buffer
- heap 不再参与(图中 heap 仅作对比)
五、Common View 的统一抽象
三阶段统一模型
Initializer
↓
Stage 1: count()
↓
Stage 2: record_allocations()
↓
Stage 3: replay_allocations(buffer)
本质是一个「两次运行 + 一次回放」
| 阶段 | 行为 |
|---|---|
| 第一次 | 只分析 |
| 第二次 | 只记录 |
| 第三次 | 真正执行 |
为什么这能解决所有难题?
回答之前的“不可能三角”
| 问题 | 解决方式 |
|---|---|
| buffer 大小未知 | Stage 2 得到 |
| constexpr 限制 | 前两阶段无分配 |
| 容器实现复杂 | 完整模拟一遍 |
六、这套方案的理论地位
这是在做什么?
把“动态分配问题”转化为“确定性脚本执行问题”
类比
- JIT:
- 分析 → 生成代码 → 执行
- 链接器:
- 符号扫描 → 地址分配 → 重定位
- 这里:
- 计数 → 记录分配 → 回放
七、最终一句话总结(非常重要)
任何 constexpr 容器,只要它的内存行为是“可重复的”,
就可以通过「计数 → 记录 → 回放」三阶段模型放入本地 buffer 中。
一、Allocator Replacement 是在解决什么问题?
背景问题
标准容器本质是:
template<typename T, typename Allocator = std::allocator<T>>
class std::vector;
Allocator 是容器类型的一部分。
这意味着:
std::vector<int, std::allocator<int>> // 一种类型
std::vector<int, MyAllocator<int>> // 另一种类型
➡ 仅仅换 allocator,类型就完全不同。
为什么要“替换 Allocator”?
在你前面讲的三阶段模型中:
- Stage 1:counter allocator
- Stage 2:transcript allocator
- Stage 3:script allocator
逻辑完全相同
元素类型完全相同
只有 allocator 不同
我们希望:
用同一个“容器逻辑类型”,只切换 allocator。
二、图的含义(Allocator Replacement SVG)
左侧
std::vector<int, std::allocator<int>>
这是原始容器类型
中间(模板展开)
template<typename T, typename Alloc>
class std::vector;
容器的真实模板参数结构
右侧
std::vector<int, Allocator<int>>
这是替换 allocator 后的新类型
➡ 本质:
类型层面“复制”容器,只换 allocator。
三、传统做法:容器内部 rebind(不推荐)
示例代码(你给的)
template<typename T, typename Allocator>
class vector {
public:
template<class Other>
struct rebind {
using other = vector<T, Other>;
};
};
使用方式
using origin_vector = std::vector<int, Allocator>;
using other_vector =
origin_vector::rebind::other<OtherAllocator>;
问题
✗ 标准容器 没有提供这种 rebind
✗ 侵入式,需要改容器实现
✗ 对第三方容器无能为力
四、通用解法:类型层面的 Allocator Replacement
核心思想
用偏特化“拆开”容器模板参数,再“重新组装”。
Step 1:声明一个工具模板
template<typename T>
struct rebind_allocator;
Step 2:对「二参数容器」做偏特化
template<
template<typename, typename> typename Container,
typename T,
typename Allocator
>
struct rebind_allocator<Container<T, Allocator>> {
template<typename NewAllocator>
using to = Container<T, NewAllocator>;
};
逐行解释
template<
template<typename, typename> typename Container,
匹配:
std::vector
std::deque
std::list
typename T,
typename Allocator
拆出:
T = int
Allocator = std::allocator<int>
using to = Container<T, NewAllocator>;
重新拼装容器类型
使用示例
template<typename T>
struct Alloc {};
using origin = std::vector<int, std::allocator<int>>;
using result =
rebind_allocator<origin>::to<Alloc<int>>;
结果等价于
std::vector<int, Alloc<int>>
五、这个技巧在三阶段 constexpr 模型中的地位
| 阶段 | allocator | 容器类型 |
|---|---|---|
| Stage 1 | counter allocator | vector<int, CounterAlloc> |
| Stage 2 | transcript allocator | vector<int, TranscriptAlloc> |
| Stage 3 | script allocator | vector<int, ScriptAlloc> |
➡ 容器“算法”完全相同
➡ 只换 allocator 类型
数学化理解
设:
- C ( T , A ) C(T, A) C(T,A) 表示容器类型
- A 1 , A 2 , A 3 A_1, A_2, A_3 A1,A2,A3 表示不同 allocator
那么:
C ( T , A 1 ) → r e b i n d C ( T , A 2 ) → r e b i n d C ( T , A 3 ) C(T, A_1) \xrightarrow{rebind} C(T, A_2) \xrightarrow{rebind} C(T, A_3) C(T,A1)rebindC(T,A2)rebindC(T,A3)
六、Initializer Passing:下一个难点
问题出现在哪?
std::vector<int> v = {1, 2, 3};
initializer_list 是:
std::initializer_list<int>
它:
- ✗ 不是 constexpr 可持久对象
- ✗ 生命周期极短
- ✗ 不能“保存”到 Stage 2 / Stage 3
三阶段中 initializer 的困境
| 阶段 | initializer |
|---|---|
| Stage 1 | 只读 |
| Stage 2 | 只读 |
| Stage 3 | ✗ 已经消失 |
七、Proposal P1045:constexpr 函数参数(被拒绝)
提案内容
void f(constexpr int x) {
static_assert(x == 5);
}
意图
强制要求参数 必须是编译期常量
希望解决的问题
- 把 initializer 的值
- 作为 constexpr 参数
- 显式传递到后续阶段
为什么被拒绝?
1⃣ 破坏函数模型
C++ 函数参数 本质是运行期实体
2⃣ constexpr 已经足够表达
constexpr int f(int x);
约束发生在 调用点,不是参数声明
3⃣ ABI / 实现复杂度过高
- 参数传值 vs 编译期替换
- 与模板系统冲突
最终结论
✗ Proposal P1045 被拒绝
八、为什么 Allocator Replacement + Initializer Passing 是“关键组合”
三阶段模型需要两样东西:
| 需求 | 解决方案 |
|---|---|
| 同一容器,多种 allocator | Allocator Replacement |
| 同一 initializer,多次使用 | 编译期结构化存储 |
当前现实
- Allocator Replacement: 已可用
- Initializer Passing:✗ 仍需 workaround
九、一句话总结(重点)
Allocator Replacement 是三阶段 constexpr 容器模型的“类型基础设施”,
而 initializer 传递问题,正是当前 C++ constexpr 能力的天花板之一。
一、核心问题一句话版
initializer 不是 core constant expression,
而 constexpr 计算的“连续性”只能由 core constant expression 保证。
因此:
initializer 在“类型 → 实例 → constexpr 值”的传递链中必然断裂。
二、第一张图:initializer ≠ core constant expression
图中结构含义
上半部分(浅绿色)
initializer
代表:
{ 1, 2, 3 }
或:
return { 1 };
下半部分(浅黄色)
core constant expression
代表:
constexpr intconstexpr std::arrayconstexpr struct { ... }
中间的 ✗(红叉)
表示:
initializer 不能直接成为 core constant expression
语言规则层面的原因
initializer 的本质
std::initializer_list<T>
它是:
- 一个 临时对象
- 持有:
- 指向编译器生成数组的指针
- 一个长度
lifetime(右侧绿色箭头)
initializer 的生命周期:
- 只保证在“完整表达式”内有效
- 不能跨函数
- 不能跨 constexpr 阶段
数学化描述
设:
- I I I = initializer
- C C C = core constant expression
则:
I ⊄ C I \not\subset C I⊂C
initializer 不属于 core constant expression 的集合。
三、第二张图:initializer → type → core constant 的断链
这张图非常关键。
左侧:Type 区域
Type
表示:
decltype(...)
类型系统阶段。
右上:initializer
{ 1 }
可以用于:
decltype(std::vector<int>{1})
initializer 可以参与类型推导
右下:core constant expression
表示:
constexpr auto x = ...;
三条箭头的含义
① initializer → Type()
decltype(std::vector<int>{1})
合法。
initializer 参与类型形成。
② Type → initializer()
std::vector<int> v = {1};
类型实例化时,可以用 initializer。
③ initializer → core constant expression(✗)
红叉位置:
initializer 不能直接变成 constexpr 值
数学关系总结
initializer → type OK \text{initializer} \xrightarrow{\text{type}} \text{OK} initializertypeOK
initializer → constexpr value NO \text{initializer} \xrightarrow{\text{constexpr value}} \text{NO} initializerconstexpr valueNO
四、第三张图:真实代码为什么失败
代码原文
struct GetInit {
std::vector<int> get() {
return { 1 };
}
};
图中三层 initializer
第一层(构造时)
return { 1 };
- initializer 用于构造
std::vector<int> - 合法
第二层(实例化时)
std::vector<int> get();
- 返回值类型确定
- 合法
第三层(constexpr 语境)
constexpr auto v = GetInit{}.get();
这里失败
为什么第三层失败?
原因 1:vector 不是字面量类型
即便忽略 allocator 问题:
std::vector
✗ 不是 literal type
✗ 内部有动态分配
原因 2:initializer 已经“消失”
initializer 的生命周期:
{1} → 构造 vector → 表达式结束 → 销毁
而 constexpr 需要:
整个求值过程中的所有对象都活着
用生命周期箭头解释
图中右侧绿色 lifetime:
- initializer 在 顶部就结束
- constexpr 计算在 底部才发生
数学化描述
设:
- t i t_i ti = initializer 生命周期
- t c t_c tc = constexpr 计算时间点
则:
t i < t c t_i < t_c ti<tc
initializer 在 constexpr 之前已经死亡。
五、为什么 allocator trick 也救不了 initializer?
即使你已经有:
- constexpr allocator
- constexpr vector
- allocator replacement
仍然失败,因为:
initializer 不属于 allocator 能控制的那一层。
allocator 能做什么?
- 控制内存来源
- 控制对象存储位置
- 控制生命周期 之后 的行为
allocator 不能做什么?
- 延长 initializer 的生命周期
- 把 initializer 变成 core constant expression
六、为什么 “constexpr 参数提案”曾被寄予厚望?
你之前提到的 Proposal P1045,本质是想做:
void f(constexpr int x);
语义是:
“这个参数 本身 就是 core constant expression”
如果成立,就可以:
f(1); // OK
f({1,2,3}); // 理论上可以拆解
➡ initializer 就能“被参数化传递”。
但被拒绝的根因(和这里完全一致)
initializer 无法成为可持久的 core constant entity
七、最终总结(非常重要)
一句话真相
initializer 是“语法糖”,不是“值对象”。
更精确地说
- initializer:
- 用于构造
- 用于类型推导
- ✗ 不能作为 constexpr 的“值载体”
形式化总结
initializer ∈ syntax \text{initializer} \in \text{syntax} initializer∈syntax
core constant expression ∈ semantic value \text{core constant expression} \in \text{semantic value} core constant expression∈semantic value
一、先读图:这张图在“证明什么”?
这张图想证明的不是:
“std::vector 不能 constexpr”
而是更深的一点:
initializer 在 “类型 → 计算 → constexpr 值” 这条链路中,
无法作为可传递的中间表示。
二、图的三大区域(非常关键)
1⃣ 左侧粉色:Types(类型世界)
types
这里是:
decltype- 模板参数
- 返回类型
operator()()的签名
只关心“是什么类型”,不关心值。
2⃣ 右上浅绿:initializer 出现的地方
[](){ return std::vector<int>{1}; }
initializer {1} 出现在:
- lambda 的返回语句中
- 用来构造一个临时
std::vector<int>
3⃣ 右下浅黄:core constant expression
constexpr int n = count(T()());
这里要求:
所有参与计算的对象,
都必须是 core constant expression 可见的值。
三、左侧代码块:initializer 的“出生点”
图中左侧代码(粉色区域)
struct UniqueName {
std::vector<int> operator()() {
return {1}; // ← initializer 在这里
}
};
关键事实
{1}是 initializer- 它:
- 合法
- 构造了一个
std::vector<int> - ✗ 本身不是一个值对象
生命周期(右侧绿色箭头)
initializer 的生命周期:
{1} ──► 构造 vector ──► 语句结束 ──► 销毁
四、右上:看起来“很 constexpr”的代码
图中代码
constexpr auto V = make(
[](){ return std::vector<int>{1}; }
);
template<typename T>
constexpr Result make(T);
乍一看:
- lambda 是 constexpr
- make 是 constexpr
- 返回值是 constexpr
看起来“全 constexpr”
五、关键断点:initializer 无法跨过“值传递边界”
图中核心计算路径
constexpr int n = count(T()());
配套函数:
constexpr int count(std::vector<int> init) {
int res = 0;
// some magic with init
return res;
}
你“以为”的数据流
{1}
↓
std::vector<int>
↓
传给 count
↓
constexpr int
实际的数据流(图中箭头的真实含义)
initializer
↓(构造)
临时 vector
↓(传参)
运行期对象
✘
constexpr 计算
六、为什么这里必然失败?(核心原因)
✗ 原因 1:initializer 不是值
initializer:
- 没有独立身份
- 不能被绑定
- 不能被存储
- 不能跨作用域
形式化表示:
initializer ∉ values \text{initializer} \notin \text{values} initializer∈/values
✗ 原因 2:vector 参数要求“持久值”
count 的签名:
constexpr int count(std::vector<int> init)
这意味着:
- 需要一个 完整对象
- 生命周期必须覆盖整个 constexpr 计算
但 initializer 的生命周期:
t init < t constexpr t_{\text{init}} < t_{\text{constexpr}} tinit<tconstexpr
✗ 原因 3:constexpr 计算禁止“隐式运行期构造”
即使在 C++20 之后:
- constexpr 允许 临时分配(transient allocation)
- 但仍要求:
- 构造
- 传参
- 析构
全部发生在 constexpr 语义内
initializer 不满足这个前提。
七、为什么“类型阶段”是可以的?
图中很多箭头指向 types 区域。
这是因为:
decltype([](){ return std::vector<int>{1}; }())
是合法的。
原因
- 类型阶段只关心:
std::vector<int>
- 不关心
{1}的值来源
数学表示:
initializer → type erasure OK \text{initializer} \xrightarrow{\text{type erasure}} \text{OK} initializertype erasureOK
八、为什么 allocator / constexpr vector 也救不了?
即使你有:
- constexpr allocator
- constexpr vector(未来)
- local buffer
仍然无解,因为:
initializer 不是 allocator 的问题,是“值模型”的问题。
allocator 能控制:
- 内存在哪
- 生命周期何时结束
但不能: - 让 initializer 变成可传递的值
九、图中这条“失败链”的本质总结
这张图证明的是:
constexpr 不是“写了 constexpr 就行”,
而是一条必须连续的值语义链。
形式化总结
设:
- I I I = initializer
- V V V = 值对象
- C C C = core constant expression
则当前语言规则是:
I → V (仅限构造时) I \to V \quad \text{(仅限构造时)} I→V(仅限构造时)
但:
I ↛ C I \not\to C I→C
因此:
I → V → C 不成立 I \to V \to C \quad \text{不成立} I→V→C不成立
十、一句话终极结论
initializer 是“构造指令”,不是“值”。
而 constexpr 只接受“值的连续传递”。
// ============================================================
// 引入 too_constexpr 库
// 该库的核心思想:
// - 在 constexpr 上下文中运行一段“看似运行期”的代码
// - 记录所有动态内存分配请求
// - 自动生成一个 constexpr 可见的本地 buffer
// - 用自定义 allocator 重新构造对象
// ============================================================
#include "https://raw.githubusercontent.com/sssersh/too_constexpr/refs/heads/master/include/cant.h"
#include <string>
#include <string_view>
#include <type_traits>
// ============================================================
// 示例 1:在 constexpr 中构造 std::string
// ============================================================
// cant::too_constexpr 接受一个 lambda
// 该 lambda 会在“编译期模拟运行期”中被执行
constexpr auto constexpr_string = cant::too_constexpr(
[]() -> std::string
{
// 正常情况下:
// std::string 需要堆分配,constexpr 中是非法的
//
// 在 too_constexpr 中:
// - 分配行为被记录
// - 实际内存来自编译期生成的本地 buffer
return "Toooooo long string just for example";
}
);
// 编译期比较字符串内容
static_assert(
constexpr_string == "Toooooo long string just for example",
"Strings are not same"
);
// ============================================================
// 验证 constexpr_string 的“真实类型”
// ============================================================
// 去掉 const / 引用,得到真正的类型
using constexpr_string_t = std::remove_cvref_t<decltype(constexpr_string)>;
// 取出该 string 使用的 allocator 类型
using magic_string_allocator_t = decltype(constexpr_string)::allocator_type;
// 证明:
// - 这是一个真正的 std::basic_string
// - 只是 allocator 被替换成了 constexpr 友好的版本
static_assert(
std::is_same_v<
constexpr_string_t,
std::basic_string<
char,
std::char_traits<char>,
magic_string_allocator_t
>
>,
"type of constexpr_string is not std::basic_string specialization"
);
// ============================================================
// 示例 2:constexpr 字符串拼接函数
// ============================================================
// 这是一个“看起来完全运行期”的函数:
// - 创建 string
// - operator+=
// - 返回结果
//
// 在标准 constexpr 中:
// ✗ 这通常是非法的(涉及动态分配)
//
// 在 too_constexpr 中:
// allocator 会自动重定向到 constexpr buffer
template<typename T1, typename T2>
constexpr std::string concat(const T1& s1, const T2& s2)
{
std::string result;
// 可能触发扩容、重新分配
result += s1;
result += s2;
return result;
}
// ============================================================
// constexpr 字符串片段(本身不分配内存)
// ============================================================
constexpr std::string_view concat1 =
"It is string which was builded ";
constexpr std::string_view concat2 =
"as concatenation of two strings";
constexpr std::string_view concat3 =
", no, as concatenation of 3 strings";
// ============================================================
// 示例 3:constexpr 拼接两个字符串
// ============================================================
constexpr auto concat_result_1 = cant::too_constexpr(
[]() -> std::string
{
// operator+ 内部会创建临时 string 并分配内存
// too_constexpr 会记录并折叠这些分配
return std::string(concat1) + std::string(concat2);
}
);
// 编译期校验拼接结果
static_assert(
concat_result_1 ==
"It is string which was builded as concatenation of two strings",
"Strings are not same"
);
// ============================================================
// 示例 4:constexpr 递归使用已有 constexpr string
// ============================================================
constexpr auto concat_result_full = cant::too_constexpr(
[]() -> std::string
{
// concat_result_1 已经是 constexpr std::string
// 现在继续在编译期进行加工
return concat(concat_result_1, concat3);
}
);
// 再次编译期验证
static_assert(
concat_result_full ==
"It is string which was builded as concatenation of two strings, "
"no, as concatenation of 3 strings",
"Strings are not same"
);
// ============================================================
// 示例 5:constexpr 中修改字符串(find + erase)
// ============================================================
// 原始字符串(只读,不分配)
constexpr std::string_view string_for_remove =
"It is string which builded (remove me)as constexpr";
// constexpr 函数:
// - 构造 std::string
// - 查找子串
// - 擦除子串
constexpr std::string remove_trash(std::string_view origin)
{
// 使用迭代器构造 string
// 会触发一次分配(由 magic allocator 处理)
std::string result{
origin.begin(),
origin.end()
};
std::string_view for_remove = "(remove me)";
// constexpr find
auto it = result.find(for_remove);
if (it != std::string::npos)
{
// constexpr erase
result.erase(it, for_remove.size());
}
return result;
}
// 在编译期调用 remove_trash
constexpr auto remove_result =
cant::too_constexpr(
[]() {
return remove_trash(string_for_remove);
}
);
// 编译期验证字符串修改结果
static_assert(
remove_result ==
"It is string which builded as constexpr",
"Strings are not same"
);
// ============================================================
// main 函数为空
// 因为:
// - 所有计算都在编译期完成
// - 运行期没有任何逻辑
// ============================================================
int main()
{
}
一、整体在做什么?(先给直觉)
一句话概括:
利用
cant::too_constexpr,在编译期“模拟运行期”,
让std::string像std::array一样在 constexpr 中工作。
二、引入库:constexpr 的“黑魔法来源”
#include "https://raw.githubusercontent.com/sssersh/too_constexpr/refs/heads/master/include/cant.h"
这行做了什么?
- 引入 too_constexpr 库
- 该库的核心作用是:
- 在 constexpr 上下文中
- 接管 allocator
- 记录分配请求
- 把动态分配“折叠”为一个静态缓冲区
这正是你前面所有 slide 里反复讨论的:
“Create the object in two steps
- Define the buffer size
- Create the target object using the defined size.”
三、最简单示例:constexpr std::string
constexpr auto constexpr_string = cant::too_constexpr(
[]() -> std::string
{
return "Toooooo long string just for example";
}
);
逐层解释
1⃣ lambda 返回 std::string
[]() -> std::string
{
return "Toooooo long string just for example";
}
- 正常情况下:✗
std::string需要堆分配- constexpr 中禁止非 transient allocation
- 在 too_constexpr 里:
- 分配请求被记录
- 最终在一个 constexpr 可见的 buffer 中完成
2⃣ cant::too_constexpr(...)
cant::too_constexpr(lambda)
它做的事情是:
- 在 constexpr 环境下运行 lambda
- 记录:
- 分配次数
- 分配大小
- 自动生成:
- 一个定长本地 buffer
- 一个特制 allocator
- 重新构造返回值
结果
constexpr_string
是一个:
std::basic_string<char, traits, magic_allocator>
而不是普通 std::string
四、编译期验证:值是否正确
static_assert(
constexpr_string == "Toooooo long string just for example",
"Strings are not same"
);
关键点
operator==在 constexpr 中执行- 字符串内容逐字符比较
- 证明:
- 内容在编译期是已知的
- 没有运行期参与
五、验证类型本身:是不是 std::string?
using constexpr_string_t = std::remove_cvref_t<decltype(constexpr_string)>;
using magic_string_allocator_t = decltype(constexpr_string)::allocator_type;
拆解
constexpr_string_t:去掉 const / 引用allocator_type:提取它用的 allocator
静态断言
static_assert(
std::is_same_v<
constexpr_string_t,
std::basic_string<char, std::char_traits<char>, magic_string_allocator_t>
>,
"type of constexpr_string is not std::basic_string specialization"
);
说明
- 不是伪造类型
- 是真·std::basic_string
- 只是:
- allocator 被替换成了 constexpr 友好的版本
六、constexpr 字符串拼接函数
template<typename T1, typename T2>
constexpr std::string concat(const T1& s1, const T2& s2)
{
std::string result;
result += s1;
result += s2;
return result;
}
为什么这是重点?
operator+=- 可能触发:
- 扩容
- realloc
- 标准 constexpr 中这是雷区
但在这里: - allocator → magic allocator
- 所有分配 → buffer 内
七、准备三个编译期字符串片段
constexpr std::string_view concat1 = "It is string which was builded ";
constexpr std::string_view concat2 = "as concatenation of two strings";
constexpr std::string_view concat3 = ", no, as concatenation of 3 strings";
为什么用 string_view?
- 本身是 constexpr
- 不涉及分配
- 作为输入非常安全
八、第一次拼接(2 段)
constexpr auto concat_result_1 = cant::too_constexpr(
[]() -> std::string
{
return std::string(concat1) + std::string(concat2);
}
);
编译期发生了什么?
- 构造
std::string(concat1) - 构造
std::string(concat2) operator+:- 分配新 buffer
- 拷贝字符
- allocator 记录总容量
- 结果折叠成 constexpr 对象
验证
static_assert(
concat_result_1 ==
"It is string which was builded as concatenation of two strings",
"Strings are not same"
);
九、第二次拼接(递归使用 constexpr 结果)
constexpr auto concat_result_full = cant::too_constexpr(
[]() -> std::string
{
return concat(concat_result_1, concat3);
}
);
关键突破点
concat_result_1:- 已经是 constexpr std::string
- 现在:
- constexpr string → constexpr function → constexpr string
这说明:
- constexpr string → constexpr function → constexpr string
too_constexpr 支持“constexpr 对象的再加工”
十、再验证一次
static_assert(
concat_result_full ==
"It is string which was builded as concatenation of two strings, no, as concatenation of 3 strings",
"Strings are not same"
);
十一、constexpr 中的字符串修改(find + erase)
输入字符串
constexpr std::string_view string_for_remove =
"It is string which builded (remove me)as constexpr";
constexpr 处理函数
constexpr std::string remove_trash(std::string_view origin)
{
// 用 string_view 构造 string(复制到可修改缓冲区)
std::string result{
string_for_remove.begin(),
string_for_remove.end()
};
std::string_view for_remove = "(remove me)";
// constexpr find
auto it = result.find(for_remove);
if (it != std::string::npos)
{
// constexpr erase
result.erase(it, for_remove.size());
}
return result;
}
这是极其重要的点
finderaseif- 迭代器
- 分支
全都在编译期完成
十二、最终 constexpr 计算
constexpr auto remove_result =
cant::too_constexpr([]() {
return remove_trash(string_for_remove);
});
验证
static_assert(
remove_result == "It is string which builded as constexpr",
"Strings are not same"
);
十三、main() 为什么是空的?
int main() {}
因为:
程序的所有“有意义工作”都发生在编译期。
运行期只剩下一个空壳。
十四、这段代码的“学术级结论”
你这段代码实际上证明了:
- C++ 标准缺的不是能力,而是机制
constexpr std::string在模型上是完全可行的- allocator replacement + 两阶段构造 是关键
- initializer、拼接、修改都能 constexpr
十五、终极一句话总结
cant::too_constexpr把 “constexpr 只能算数值”
升级成了 “constexpr 能执行一段完整程序”。
https://godbolt.org/z/6o71co8aj
// ============================================================
// 引入 too_constexpr 库
//
// too_constexpr 的核心思想:
// 1⃣ 在 constexpr 上下文中“执行”一段看似只能运行期执行的代码
// 2⃣ 拦截并记录所有动态内存分配行为(operator new / allocator)
// 3⃣ 在编译期计算出需要的总内存大小
// 4⃣ 自动生成一个 constexpr 可见的本地 buffer
// 5⃣ 使用自定义 allocator,让容器把元素放进这个本地 buffer
//
// 本质:
// 用「两阶段构造」模拟运行期的动态内存模型
// ============================================================
#include "https://raw.githubusercontent.com/sssersh/too_constexpr/refs/heads/master/include/cant.h"
#include <vector>
#include <type_traits>
// ============================================================
// 在 constexpr 中构造 std::vector<int>
// ============================================================
// constexpr_vector 是一个“编译期构造完成”的 std::vector
//
// 表面上看:
// - 使用了 std::vector
// - 使用了 initializer_list
// - 必然涉及堆分配
//
// 但实际上:
// - 分配请求被 too_constexpr 记录
// - vector 的 allocator 被替换成 magic allocator
// - 内存来自编译期生成的本地 buffer
constexpr static auto constexpr_vector =
cant::too_constexpr(
[]() -> std::vector<int>
{
// 这里的 initializer_list 会触发:
// 1. vector 构造
// 2. 分配足够的连续内存
// 3. 拷贝 / 移动元素
//
// 在标准 constexpr 中 ✗ 非法
// 在 too_constexpr 中 合法
return { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
}
);
// ============================================================
// dummy 函数:证明 constexpr_vector 是一个“正常的对象”
// ============================================================
// 这里返回 constexpr_vector,说明:
// - 它不是某种编译期假对象
// - 而是一个可以在运行期正常使用的 std::vector
auto dummy()
{
return constexpr_vector;
}
// ============================================================
// 编译期访问 vector 元素
// ============================================================
// operator[] 在 constexpr 上下文中可用
// 前提是:
// - vector 内部数据是 constexpr 可访问的
static_assert(
constexpr_vector[0] == 1,
"error"
);
// ============================================================
// 类型层面的验证
// ============================================================
// 去掉 const / 引用,得到真实类型
using constexpr_vector_t =
std::remove_cvref_t<decltype(constexpr_vector)>;
// 提取 vector 使用的 allocator 类型
using magic_vector_allocator_t =
decltype(constexpr_vector)::allocator_type;
// 验证:
// - 这是一个真正的 std::vector
// - 只是 allocator 被替换为 constexpr 友好的版本
static_assert(
std::is_same_v<
constexpr_vector_t,
std::vector<int, magic_vector_allocator_t>
>,
"type of constexpr_vector is not std::vector specialization"
);
// ============================================================
// main 函数
// ============================================================
// 所有计算都已在编译期完成
// 运行期不会再发生任何分配
int main()
{
}
更多推荐


所有评论(0)