C++26:减少未定义行为
摘要
C++26 引入了减少未定义行为的更改,特别是使删除指向不完整类型的指针成为格式错误,从而提高了程序安全性。
<p><a href="https://lobste.rs/s/xinbkz/c_26_reducing_undefined_behaviour">评论</a></p>
查看缓存全文
缓存时间: 2026/07/29 11:54
# C++26: 减少未定义行为
来源:https://www.sandordargo.com/blog/2026/07/29/cpp26-reduces-undefined-behaviour
如今,安全性是 C++ 关注的焦点。无论是在委员会会议、会议演讲还是走廊讨论中,这个话题都不断被提及。C++26 在这方面迈出了重大步伐,其中一个反复出现的主题是减少导致未定义行为的场景。
用其中一份提案的话来说,*“未定义行为几乎具有无限的可能性导致程序不良行为,包括安全漏洞、数据损坏、死锁等”*。仅仅是违反前置条件这一简单行为,就可能在不知不觉中把一个看起来完全正确的程序变成会表现出上述任何一种问题的程序。
在今天的文章中,我们来看看 C++26 是如何以及在哪里解决这个问题的。其中一些更改我们已经在博客中介绍过,但在上下文中值得重申。还有一个重要的变化我们还没有谈到。让我们从那个开始。
## P3144R2:删除指向不完整类型的指针应为非良构
在 C++26 之前,删除指向不完整类型的指针是未定义行为。唯一的例外是当完整类类型恰好具有平凡析构函数且没有类特定的释放函数时。在所有其他情况下,编译器根本不知道该怎么办。
考虑这个例子:
```cpp
// widget.h
struct Widget; // 仅前向声明
void deleteWidget(Widget* p) {
delete p; // 应该调用哪个析构函数?
// 是否有自定义的 operator delete?
}
```
```cpp
// widget.cpp
struct Widget {
~Widget() { /* 非平凡清理 */ }
static void operator delete(void* p) { /* 自定义释放 */ }
};
```
在 `delete` 表达式的位置,编译器不知道 `Widget` 是否有非平凡析构函数或自定义的 `operator delete` —— 前者相当常见,而后者相对罕见。它无法在翻译时判断程序行为是良好定义的还是未定义的 —— 这个判断依赖于通常只有链接器才可用的信息。
P3144R2(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3144r2.pdf)使删除指向不完整类类型的指针成为非良构。句号。现在这是一个编译错误。
### 走向非良构之路
有趣的是,这不是最初的方向。提案中考虑了几种替代方案。让我们列举其中一些:
- **首先弃用**:最初的方案是弃用此类删除,只在后续标准中使其成为非良构。担心直接变为非良构会面临采用阻力。
- **要求编译器做正确的事**:强制实现为不完整类型生成正确的析构函数调用和释放操作。想法不错,但没人知道如何高效实现。
- **不调用析构函数**:将行为定义为结束对象生命周期而不运行其析构函数。这与 *错误行为*(借用自 P2795R5(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2795r5.html))概念结合,并伴随弃用。
- **无条件非良构**:包括具有平凡析构函数的类型。这会破坏一些当前工作正常的已有代码,感谢 Hyrum 定律。
经过深入分析,最初偏好的解决方案是弃用路径加上错误行为。这个想法是在所有情况下保持良好定义行为,但将该模式标记为已弃用。错误行为分类将给编译器提供更好的诊断,无论是编译时还是运行时。
但事实证明,委员会更加勇敢。这种勇气基于这样一个事实:所有主要编译器已经对此类删除发出警告,甚至不尝试确定完整类型是否有平凡析构函数。换句话说,在实践中,开发者已经被告知不要这样做。所以委员会选择了最简单且最具影响力的选择:使其非良构。
如果你有通过不完整类型删除的现有代码,你需要在删除点使该类型完整。这无论如何都是正确的做法 —— 它能确保析构函数实际运行。
## P2795R5:未初始化读取的错误行为
我们之前已经详细讨论过这个(https://www.sandordargo.com/blog/2025/02/05/cpp26-erroneous-behaviour),但它完美契合减少未定义行为的主题,所以让我们回顾一下。
读取未初始化的局部变量过去是未定义行为。从 C++26 开始,它变成了*错误行为* —— 一个新的类别,表示行为是良好定义但不正确的。建议编译器对此进行诊断。
```cpp
void foo() {
int d; // d 有一个错误值
bar(d); // 错误行为,不再是 UB
}
```
程序不再静默地做任何事情(如 UB 允许的),未初始化的对象现在被初始化为一个实现特定的值。读取它是一个概念性错误,鼓励编译器通过警告或运行时错误进行诊断。
如果你出于性能原因有意让变量保持未初始化,C++26 提供了 `[[indeterminate]]` 属性。但在那种情况下,在初始化之前读取变量仍然是未定义行为 —— 你是明确选择进入它的。
这一更改的妙处在于,只需重新编译现有代码即可生效。不需要手动修改代码 —— 编译器会告诉你哪里忘了初始化变量。
## P2748R5:禁止将返回的引用绑定到临时对象
这个我们也在专门的文章中讨论过(https://www.sandordargo.com/blog/2025/06/11/cpp26-no-binding-to-returned-reference-to-temporary)。它将常见的悬垂引用模式从未定义行为转变为编译错误。
在 C++26 之前,标准说函数 return 语句中绑定到返回值的临时对象的生命周期不会延长。临时对象在 return 语句的完整表达式结束时就被销毁。这是悬垂引用的配方。
```cpp
auto&& f1() {
return 42; // 之前是 UB,现在是非良构
}
const double& f2() {
static int x = 42;
return x; // 隐式转换为 double 创建了一个临时对象
// 之前是 UB,现在是非良构
}
```
C++26 用一条清晰的规则替换了模糊的“生命周期不延长”措辞:return 语句将返回的引用绑定到临时表达式是非良构的。代码将无法编译。这就在编译时消除了整类微妙的生命周期错误,而不是让它们在运行时表现为神秘的崩溃。
## P3471R4:标准库加固
上述更改都是语言层面的。但 C++26 也通过标准库加固在库层面解决安全性问题,我们在专门的文章中介绍过(https://www.sandordargo.com/blog/2026/05/13/cpp26-library-hardening)。
加固背后的思想很简单:许多标准库操作都有前置条件,违反这些条件会导致未定义行为。当 vector 只有 5 个元素时访问 `vector[10]` 是未定义行为。在空的 `optional` 上调用 `front()` 也是如此,或者解引用无效的 `span` 索引也是如此。
P3471R4(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3471r4.pdf)标准化了一种机制,让实现能够在运行时检查这些前置条件。当检查失败时,程序可以陷阱或中止,而不是静默地损坏内存。它涵盖了容器如 `std::vector`、`std::array` 和 `std::deque`;视图如 `std::span` 和 `std::string_view`;以及包装器如 `std::optional` 和 `std::expected`。
使这种方法实用的是它是可选加入的,并且不需要更改代码 —— 你只需在启用加固的情况下重新编译(例如在 Clang 和 GCC 中使用 `-fhardened`)。实际部署报告大约有 0.3% 的性能开销,同时捕获了数千个错误。这是一个了不起的权衡。
## 结论
C++26 从多个角度处理未定义行为。在语言层面,删除指向不完整类型的指针现在是非良构的,读取未初始化变量变为错误行为而非未定义,返回对临时对象的引用是编译错误。在库层面,标准加固为实现在运行时以最小开销捕获前置条件违反提供了一种标准化方式。
这些更改都不需要新的编程范式或大规模重写。大多数只需用符合 C++26 的编译器重新编译现有代码即可。这正是重要的进步:在不要求开发者改变思维方式的情况下,让现有代码更安全。
## 深入连接
如果你喜欢这篇文章,请
- 点击点赞按钮,
- 订阅我的通讯(https://sandor-dargo.kit.com/e19f29b0a1),
- 并在 Twitter 上联系我(https://twitter.com/SandorDargo)!
- 如果你正在准备 C++ 量化/交易面试,请查看 GetCracked(https://www.getcracked.io/?via=sandor)。
相似文章
C++26:标准库强化
C++26 引入了标准化的库强化机制,用于在运行时捕获常见的未定义行为(如越界访问)。基于 Google 的生产经验,此举仅带来 0.30% 的性能开销,同时将段错误减少了 30%。
C语言中的一切皆为未定义行为
一位经验丰富的C++开发者认为,所有非平凡的C和C++代码都包含未定义行为,使得内存安全无法实现,并质疑这些语言在现代软件开发中的持续使用。
C++26 中 std::format 的改进
C++26 标准对 std::format 库进行了多项改进,包括直接指针格式化、路径格式化、constexpr 支持,以及为 std::println 新增的空行重载。
C++26:更多函数包装器
C++26 引入了两个新的函数包装器:std::copyable_function(提供了可复制且 const 正确的 std::function 替代品)和 std::function_ref(一个非拥有、可调用的引用,具有引用语义)。
Dependable C
一个全面的资源,提供编写可靠、安全的C代码的指南和建议,涵盖未定义行为、内存模型以及特定版本的注意事项。