- 编程语言
- 编译器
- 开发工具
【免费下载链接】cppfront
A personal experimental C++ Syntax 2 -> Syntax 1 compiler
本篇技术指南以 cppfront 仓库中的 docs/cpp2/safety.md 为核心骨架,系统讲解 Cpp2 语言「默认安全(safe by default)」的设计:哪些不安全操作会被编译期拒绝、哪些会被注入运行期检查,以及如何通过unchecked_*函数(局部退出)与-no-*-checks编译开关(文件级退出)两种方式精准豁免。读完本文,你将掌握混合符号比较、除零、空指针解引用、下标越界四类安全检查的触发条件、豁免手段与底层实现原理,能够在热循环等真实性能敏感场景中做出合理的取舍。
一、总体设计:默认安全,两级豁免
Cpp2 的设计目标是在默认情况下保持安全,且绝大多数安全检查在编译期完成;仅在无法静态保证时,才退而在运行期注入检查。文档原文明确了两条豁免途径:
- 代码内定点豁免:在确信安全的特定位置,使用
cpp2::命名空间下的unchecked_*函数(Cpp2 代码中可直接不带限定名使用); - 整文件豁免:对某个源文件整体使用
-no-*-checks系列编译开关。
文档同时给出重要实践建议:几乎总是应该选择在代码中定点豁免,前提是你对结果有十足把握;而运行期检查(如下标越界)只有当你在热循环中实测到性能差异时,才值得豁免。下文将逐一剖析四类检查。
二、混合符号整数比较(编译期强制)
检查规则
直接使用<、<=、>、>=比较有符号与无符号整数,可能因隐式符号转换得出错误结果(例如-1 < 1u实际为假),因此 cppfront在编译期直接拒绝这类比较。这是四类检查中唯一纯编译期(zero runtime cost)的一项。
定点豁免:unchecked_cmp_*
在确信类型转换不会出错的位置,用对应函数替代运算符:
main: () = { x: i32 = 42; y: u32 = 43; if x < y { } // unsafe, therefore error by default if unchecked_cmp_less(x,y) { } // ok, explicit "trust me" opt-out }四个函数分别对应四个运算符:unchecked_cmp_less、unchecked_cmp_less_eq、unchecked_cmp_greater、unchecked_cmp_greater_eq。
文件级豁免:-no-comparison-checks
cppfront myfile.cpp2 # mixed-sign int comparisons banned cppfront myfile.cpp2 -no-comparison-checks # mixed-sign int comparisons allowed该开关还有短别名-no-c(见 docs/cppfront/options.md)。
源码实现原理
在 include/cpp2util.h 中,四个unchecked_cmp_*均为CPP2_FORCE_INLINE constexpr模板,带requires requires { CPP2_FORWARD(t) < CPP2_FORWARD(u); }约束,直接完美转发并执行比较,不做任何检查。
而被禁止的运算符版本则经由impl::cmp_mixed_signedness_check<T,U>()把关(include/cpp2util.h):
- 若两侧都是整型且符号性不同,触发
static_assert,报错信息提示改用.ssize()、std::cmp_less,或通过as/cpp2::unchecked_narrow显式统一符号性; - 特殊情形:bool 值之间使用
< <= >= >也会被拒绝,报错提示「是否漏了括号」(用于捕获(x < y) < z这类可疑写法)。
源码注释还解释了为何不做「静默修复」(即自动改用std::cmp_*):那样会带来安全陷阱(对应上游 issue #220 的讨论),因此显式拒绝才是正确策略。
三、除零检查(运行期强制)
检查规则
整数除以零属于未定义行为。由于除数为零在编译期通常无法确定,cppfront 在运行期检查分母是否为非零值。
定点豁免:unchecked_div
main: () = { x := 42; y := 0; z := x/y; // unsafe, therefore run-time checked w := unchecked_div(x,y) // ok, explicit "trust me" opt-out }文件级豁免:-no-div-zero-checks
cppfront myfile.cpp2 # division by zero checked cppfront myfile.cpp2 -no-div-zero-checks # division by zero not checked该开关短别名为-no-d(见 docs/cppfront/options.md)。
源码实现原理
unchecked_div同样位于 include/cpp2util.h,是CPP2_FORCE_INLINE constexpr的除法转发;而安全版本的/在 source/to_cpp1.h 附近依据flag_safe_zero_division注入运行时分母检查(分母为 0 时触发断言/异常,未定义行为被转换为可诊断的失败)。
四、空指针解引用检查(运行期强制)
检查规则
解引用空指针是未定义行为,cppfront 在运行期检查指针非空后再解引用。
定点豁免:unchecked_dereference
main: () = { p: *int = cpp1_func(); // could be initialized to null p* = 42; // unsafe, therefore run-time checked unchecked_dereference(p) = 42; // ok, explicit "trust me" opt-out }注意 Cpp2 中解引用后缀写法为p*,豁免时用unchecked_dereference(p)(返回可赋值的引用)。
文件级豁免:-no-null-checks
cppfront myfile.cpp2 # null dereferences checked cppfront myfile.cpp2 -no-null-checks # null dereferences not checked该开关短别名为-no-n(见 docs/cppfront/options.md)。
源码实现原理
unchecked_dereference在 include/cpp2util.h,requires requires {*CPP2_FORWARD(p);}后直接返回*p。- 安全解引用的注入点位于 source/to_cpp1.h:当
flag_safe_null_pointers为真且当前词法记号是Multiply(即p*后缀解引用)时,编译器在表达式前缀插入cpp2::impl::assert_not_null(、后缀补),将p*改写为「先断言非空、再解引用」的安全形式。
五、下标越界检查(运行期强制)
检查规则
越界下标访问是未定义行为。对表达式a[b],若满足:
a是连续存储且支持std::size(a);b是整型值;
则 cppfront 会在调用a[b]前注入检查:0 <= b < std::size(a)。
定点豁免:unchecked_subscript
main: () = { v: std::vector = ( 1, 2, 3, 4, 5 ); s: std::span = v; idx := calc_index(s); v[idx] += 42; // unsafe, therefore run-time checked s[idx] += 84; // unsafe, therefore run-time checked // manually hoist the check and do it myself if (0 ..< v.size()).contains(idx) { unchecked_subscript(v,idx) += 42; // ok, explicit "trust me" opt-out unchecked_subscript(s,idx) += 84; // ok, explicit "trust me" opt-out } }示例展示了两种典型配合:既可以直接用unchecked_subscript跳过检查,也可以先用 Cpp2 的半开区间0 ..< v.size()配合.contains(idx)手动上提(hoist)检查,再把循环体内的访问降级为 unchecked 版本——这是文档推荐的「把检查移出热循环」的典型模式。
文件级豁免:-no-subscript-checks
cppfront myfile.cpp2 # subscript bounds checked cppfront myfile.cpp2 -no-subscript-checks # subscript bounds not checked该开关短别名为-no-s(见 docs/cppfront/options.md)。
源码实现原理
unchecked_subscript在 include/cpp2util.h,约束为requires { CPP2_FORWARD(a)[b]; },直接返回a[b],零检查、零开销。- 安全下标的注入点在 source/to_cpp1.h:当
flag_safe_subscripts为真、下标为单个整型表达式(LeftBracket且表达式列表长度为 1)时,编译器在表达式前插入CPP2_ASSERT_IN_BOUNDS(,并追加, <下标表达式>与右括号,把a[b]改写为带边界断言的调用;若下标是十进制整型字面量(如a[5]),则优化为更轻量的CPP2_ASSERT_IN_BOUNDS_LITERAL(版本——因为常量下标在运行期可做一次求值缓存,且编译器能更早暴露常量越界错误。
六、开关实现:-no-*-checks的解析与默认值
四个开关全部定义在 source/to_cpp1.h,底层是四个默认true的布尔标志,命令行注册顺序与文档一一对应:
| 开关 | 短别名 | 控制标志(默认) | 作用 |
|---|---|---|---|
-no-null-checks | -no-n | flag_safe_null_pointers = true | 关闭空指针解引用检查 |
-no-div-zero-checks | -no-d | flag_safe_zero_division = true | 关闭整数除零检查 |
-no-subscript-checks | -no-s | flag_safe_subscripts = true | 关闭下标越界检查 |
-no-comparison-checks | -no-c | flag_safe_comparisons = true | 关闭混合符号比较诊断 |
关键设计:标志默认均为true,即「安全默认开启」;用户传入开关后标志置false,相应的代码生成路径(如 source/to_cpp1.h 的assert_not_null注入、source/to_cpp1.h 的CPP2_ASSERT_IN_BOUNDS注入)被整体跳过。这印证了「默认安全、显式退出」的架构:你在命令行显式写的每个-no-*都是在为整份文件承担安全责任。
七、退出方式的选择与最佳实践
综合文档建议与实现,选择豁免方式时应遵循以下排序:
- 首选定点
unchecked_*:语义自文档化(代码读者看到unchecked_前缀即知此处刻意跳过检查),影响范围最小,且便于日后审计与回归; - 必要时先手动上提检查:如下标示例所示,用区间
contains把检查移到循环外,再在循环体内用unchecked_subscript,兼得安全与性能; - 最后才考虑
-no-*-checks:仅当某文件整体确定安全、或运行期检查在热路径上产生可实测的开销差异时使用,且应在命令行注释中说明原因。
始终牢记:豁免的前提是「你确信结果安全」——unchecked_*本质是向编译器声明「trust me」,把未定义行为的风险从编译器转移回程序员。
八、相关测试与进一步阅读
仓库回归测试对四类安全机制均有覆盖,可作为行为契约参考:
- 下标越界:
mixed-bounds-check.cpp2、pure2-bounds-safety-span.cpp2(其预期输出见对应.execution文件); - 空指针断言族:
pure2-assert-unique-ptr-not-null.cpp2、pure2-assert-shared-ptr-not-null.cpp2、pure2-assert-optional-not-null.cpp2、pure2-assert-expected-not-null.cpp2; - 安全相关的编译错误用例:
mixed-initialization-safety-1-error.cpp2、pure2-lifetime-safety-reject-null-error.cpp2等(对应.cpp2.output记录诊断文本)。
更完整的开关说明可参考 docs/cppfront/options.md;unchecked_*的全部实现集中在 include/cpp2util.h。若想追踪安全开关如何影响代码生成,可从 source/to_cpp1.h 的标志定义出发,沿flag_safe_*的使用点(source/to_cpp1.h)逐行阅读注入逻辑,即可完整还原 cppfront 的「默认安全 → 显式退出」全链路。
- 编程语言
- 编译器
- 开发工具
【免费下载链接】cppfront
A personal experimental C++ Syntax 2 -> Syntax 1 compiler
相关推荐
MyTinySTL中的内存对齐检查:运行时与编译期
MyTinySTL中的内存对齐检查:运行时与编译期 你是否曾因内存对齐问题导致程序崩溃或性能下降?在C++开发中,内存对齐是影响程序稳定性和效率的关键因素。本文
标准库fmt类型安全机制:编译期类型检查的深层原理
fmt类型安全机制:编译期类型检查的深层原理 fmt库作为现代C++格式化库的标杆,其最强大的特性之一就是 编译期类型安全检查 机制。这项技术让开发者能够在编译
标准库EAGLE快速入门:如何在8x RTX 3090上1-2天完成训练
EAGLE快速入门:如何在8x RTX 3090上1 2天完成训练 EAGLE(GitHub 加速计划)是一个高效的大模型训练框架,支持EAGLE 1(ICML
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考