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 中的内存分配:总体概念

Compile time evaluation Compile time heap constexpr void foo() { int *p1; } Compile time Compile time evaluation Compile time heap constexpr void foo() { int *p1; p1 =new int[5]; } Compile time Compile time evaluation Compile time heap constexpr void foo() { int *p1; delete[] p1; p1 =new int[5]; } Compile time Compile time evaluation Compile time heap constexpr void foo() { int *p1; delete[] p1; p1 =new int[5]; } Compile time Compile time evaluation Compile time heap Compile time heap constexpr void foo() { int *p1; delete[] p1; p1 =new int[5]; } Compile time

在 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];
}

发生了什么?

  1. 编译器在 编译期堆(Compile time heap) 中:
    • 分配 5 个 int
  2. p1 指向这块编译期内存
    问题来了
  • 这块内存 没有释放
  • p1 是局部变量,但内存泄漏发生在 编译期
    非法:Non-transient allocation

五、第三张图:new + delete(正确的 Transient allocation)

constexpr void foo() {
    int* p1;
    p1 = new int[5];
    delete[] p1;
}

编译期行为顺序

  1. new int[5]
    • 编译期堆 分配内存
  2. 使用(即使什么都不做也没关系)
  3. delete[] p1
    • 在同一次 constexpr 求值中释放
      合法
      典型的 Transient constexpr allocation

六、为什么必须成对出现?

编译期的“内存守恒规则”

你可以把 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 分配)

Compile time evaluation Compile time heap constexpr int* foo() { constexpr int *p1 = foo(); return new int[5]; } Compile time Compile time evaluation Compile time heap constexpr int* foo() { constexpr int *p1 = foo(); return new int[5]; } Compile time Compile time evaluation Compile time heap Runtime evaluation Runtime evaluation constexpr int* foo() { constexpr int *p1 = foo(); constexpr int *p1; return new int[5]; } Compile time Runtime time

先给一句标准语义层面的定义

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,Rallocated 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();

编译器就必须回答这些问题:

  1. 这块内存放哪?
    • .rodata
    • .data
    • runtime heap?
  2. 生命周期是谁管理?
  3. 谁来 delete?
  4. 多个单元是否共享?
  5. 指针值是否稳定?
    这些问题一旦允许 → 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 lifetimeconstexpr 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 lifetimeconstexpr 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 / catchconstexpr

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 persistenceODR + 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 lifetimeheap lifetime
所以,只有“不用 heap 的容器”才能成为 top-level constexpr 对象

二、哪些容器是「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::array qualifies
原因总结:


条件 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 allocmust 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 heapstandard 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 };
std::initializer_list size = 6 != 8 capacity std::vector data memory size = 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} capacitysize
且:
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 countcompile-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 构造到底做了什么?

container(init_type init) { ... data1 = a.allocate(1); data2 = a.allocate(2); data3 = a.allocate(4); a.deallocate(data1, 1); ... } container data1 data2 data3 allocator heap ? result = 6 intermediate = 7 pair allocator initializer ? pairs => nodes + pointers std::unordered_map pointer allocator node allocator data heap

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 pair1 node+1 pointer

3⃣ 图中公式的真实含义

你图里这部分:

pairs  =>  nodes + pointers

等价于:
N  pairs ⇒ N  nodes + N  pointers N\ \text{pairs} \Rightarrow N\ \text{nodes} + N\ \text{pointers} N pairsN 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 footprintcompile-time bound

六、最终总结(高度凝练)

std::unordered_mapinitializer_list 构造
是一个「语义上简单、实现上极度复杂」的接口。

它的问题不在 API,而在于:

  • 没有容量语义
  • 没有 allocation 上界
  • 必须逐个 insert
  • rehash 行为完全不可预测

一句话终结版

Initializer-list construction of unordered_map implies 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.

一、这张图整体在解决什么问题?

124 initializer 2 container transcript allocator table n data1 data2 data3 initializer 3 container script allocator table buffer n s data1 data2 data3 heap initializer 1 container counter allocator n = 3 data1 data2 data3 heap heap

一句话概括

把“容器构造”拆成多个阶段,每个阶段只做一件事:

  • 阶段 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 内部有:
    • table
    • n
  • 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=ialign_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 pks

此时发生了什么?

  • 真正构造 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 是在解决什么问题?

std::vector< int, std::allocator<int> > Allocator<int> template<typename T, typename Alloc> std::vector int std::allocator<int> std::vector< int, Allocator<int> >

背景问题

标准容器本质是:

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)rebind C(T,A2)rebind C(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 core constant expression lifetime Type initializer core constant expression lifetime types core constant expression struct GetInit { std::vector<int> get() { return { 1 }; } }; initializer initializer core constant expression "decltype" instantiate get() lifetime

initializer 不是 core constant expression,
而 constexpr 计算的“连续性”只能由 core constant expression 保证。

因此:
initializer 在“类型 → 实例 → constexpr 值”的传递链中必然断裂。

二、第一张图:initializer ≠ core constant expression

图中结构含义

上半部分(浅绿色)

initializer

代表:

{ 1, 2, 3 }

或:

return { 1 };

下半部分(浅黄色)

core constant expression

代表:

  • constexpr int
  • constexpr std::array
  • constexpr 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 IC
    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} initializertype OK
initializer → constexpr value NO \text{initializer} \xrightarrow{\text{constexpr value}} \text{NO} initializerconstexpr value NO

四、第三张图:真实代码为什么失败

代码原文

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} initializersyntax
core constant expression ∈ semantic value \text{core constant expression} \in \text{semantic value} core constant expressionsemantic value

一、先读图:这张图在“证明什么”?

types core constant expression lifetime struct UniqueName { ... std::vector<int> operator()() { return {1}; } ... }; constexpr int n = count(T()()); } constexpr int count(std::vector<int> init){ int res =0; // some magic with init return res ; } constexpr auto V = make( [](){return std::vector<int>{1};} ); template<typename T constexpr Result make(T){

这张图想证明的不是:

“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 erasure OK

八、为什么 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{(仅限构造时)} IV(仅限构造时)
    但:
    I ↛ C I \not\to C IC
    因此:
    I → V → C 不成立 I \to V \to C \quad \text{不成立} IVC不成立

十、一句话终极结论

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::stringstd::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

  1. Define the buffer size
  2. 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)

它做的事情是:

  1. 在 constexpr 环境下运行 lambda
  2. 记录:
    • 分配次数
    • 分配大小
  3. 自动生成:
    • 一个定长本地 buffer
    • 一个特制 allocator
  4. 重新构造返回值

结果

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);
    }
);

编译期发生了什么?

  1. 构造 std::string(concat1)
  2. 构造 std::string(concat2)
  3. operator+
    • 分配新 buffer
    • 拷贝字符
  4. allocator 记录总容量
  5. 结果折叠成 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
      这说明:

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;
}

这是极其重要的点

  • find
  • erase
  • if
  • 迭代器
  • 分支
    全都在编译期完成

十二、最终 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() {}

因为:

程序的所有“有意义工作”都发生在编译期。
运行期只剩下一个空壳。

十四、这段代码的“学术级结论”

你这段代码实际上证明了:

  1. C++ 标准缺的不是能力,而是机制
  2. constexpr std::string 在模型上是完全可行的
  3. allocator replacement + 两阶段构造 是关键
  4. 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()
{
}

https://godbolt.org/z/nMWb3onbE

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐