文件编号 | SD-10 |
---|---|
日期 | 2024-12-02 |
回复给 | EWG 主席 |
1 动机
本常设文件汇集了指导新提案及其讨论的原则,语言演进工作组(EWG)可以在具体情况下(见下文)明确偏离这些原则。
本常设文件是由 EWG 维护的活文档。请通过 EWG 论文提出改进/扩展建议。
在本文件中,“安全性”不加限定地指编程语言安全性(例如,类型安全和内存安全),这既支持软件安全(保护资产的能力),也支持软件安全性(例如,生命安全)。
2 一般原则:默认设计,明确例外
本常设文件中的原则旨在表达我们强烈尝试遵循的指导方针。在具体情况下,我们可能会选择破例并出于充分理由推翻某项指导方针,但如果这样做,我们应该 (a) 讨论并记录导致我们考虑破例的明确设计权衡理由,并且 (b) 在可能的情况下,为用户提供一种“打开引擎盖并进行控制”的方式,并选择恢复到默认设计(例如,对于可能产生性能成本的功能,如果在热循环中需要,提供一种选择退出的方式)。
3 来自 [D&E] 的原则
这些原则重申了 [D&E] 第 4.5 节的关键原则列表,并进行了 [括号] 中所示的细微更新。
3.1 “保留与 C 的链接兼容性”[和以前的 C++]
这属于“使用传统(笨拙)链接器”之下,但主要重点仍然适用。
与旧版 C++ 的 100% 无缝、无摩擦的链接兼容性应เป็น默认要求。我们可以在具体情况下(或甚至整体上)决定进行 ABI 破坏性更改,但应明确讨论并记录原因。
示例:我们不应要求包装器/跳板/适配器来使用以前标准的标准库。
示例:我们不应在知道需要 ABI 破坏的情况下进行更改,除非明确将其批准为例外。例如,在 C++11 中,我们知道我们对 `basic_string` 进行了 API 更改,这在某些实现中会需要 ABI 破坏。
3.2 “不与 C [或以前的 C++] 产生无谓的不兼容性”
与旧版 C++ 的 100% 无缝、无摩擦的源代码兼容性必须始终可用。
示例:我们可以采用“版本”或替代语法,只要存在一种现有语法可用的模式。例如,我们添加了 using 别名,它涵盖了 typedef 的所有用途,未来我们可以在一种语法模式下,允许 using 且拒绝 typedef,但前提是允许所有现有代码(包括 typedef)的模式仍然可用。(另请参阅 3.5。)
示例:我们不应分叉标准库,例如拥有两种相互竞争的 vector 类型或两种 span 类型(例如,现有类型和用于安全代码的不同类型),这会在组合使用这两种类型的代码(尤其是在函数签名中)时造成困难。
3.3 “不为低于 C++ 的低级语言留下空间(汇编除外)”
Stroustrup 在 [D&E] 中说得很好:“要保持 C++ 作为一种可行的系统编程语言,C++ 必须保持 C 直接访问硬件、控制数据结构布局以及拥有与硬件一一对应的原始操作和数据类型的能力。替代方案是使用 C 或汇编。语言设计任务是隔离低级功能,并使它们对于不直接处理系统细节的代码来说是不必要的。目标是保护程序员免受意外误用,同时不施加不必要的负担。”
另请参阅 #5。
3.4 “不使用,不付费(零开销原则)”
第二部分是:如果您使用它,它的效率就像您手工编写的一样(零开销抽象原则)。
3.5 “如果怀疑,提供手动控制方式”
始终让程序员说“相信我”并“打开引擎盖”以完全控制。
注意:这与默认的语言安全性并不矛盾。情况恰恰相反:始终有办法选择退出以获得完全的性能和控制,这使得默认情况下可以使更多事物安全。
示例: C++26 默认将读取未初始化局部变量设为错误行为(良好的安全默认,无未定义行为),但提供了一种方式让程序员通过编写 [[indeterminate]]
来明确选择退出。
4 附加原则
“在 C++ 内部,存在一种更小更简洁的语言,它正在努力挣脱束缚。” — B. Stroustrup [D&E]
“C++ 大小的 10%……大部分的简化将来自通用化。” — B. Stroustrup [HOPL-III]
EWG 还采纳以下附加原则。
4.1 默认情况下使功能安全,并通过选择退出始终提供完整的性能和控制
我们希望 C++ 仍然提供完整的性能和控制,但不要让语言不安全错误经常因误操作而容易编写(最初优先考虑类型、边界、初始化和生命周期安全,然后也进展到其他语言安全类别)。当我们添加方法使其更容易在默认“如果编译则安全/更安全”的模式下编译 C++ 时(例如,C++26 模式下未初始化局部变量没有未定义行为,或在启用配置文件模式下),我们还为程序员提供了明确说“相信我”并仍然在需要的地方策略性地使用危险构造的方式(例如,通过提供一种语法来抑制热循环中某一行代码的边界安全配置文件,当程序员已通过其他方式验证了边界安全时)。
示例(重述): C++26 默认将读取未初始化变量设为错误行为(良好的安全默认,无未定义行为),但提供了一种方式让程序员通过编写 [[indeterminate]]
来明确选择退出。
4.2 偏爱通用功能,避免“狭窄”的特殊情况功能
我们不应该添加仅在语言的某个部分而不是其他部分起作用的特殊“狭窄”功能。正如 Stroustrup 在 [HOPL-IV] 第 6.1 节中关于概念所说,“我想要……充分的通用性/表达性——我明确不想要只能表达我能想象到的东西的功能。”
注意:发布一个通用功能的初始版本,它只支持一部分用例(例如,我们对 constexpr
所做的那样),是没问题的,只要有明确的路径在语言中实现更通用的支持。
4.3 偏爱直接表达意图的功能:“是什么,而不是怎么做”
简化代码(并实现正确性、工具、优化、构建吞吐量)的方法是使程序员能够直接表达其意图。这是将编码习惯/模式的样板代码提升为更简洁、更快、更可靠代码的方式。正如 Stroustrup 在 [D&E] 第 4.3 节中所说:“说出你的意思”,并在 [HOPL-IV] 第 2.1 节中详细阐述为“说出你的意思(即,实现更高层次思想的直接表达)。”
注意:每次我们添加这样的功能,我们都会使 C++ 的使用变得更简单。
注意:我们可以在哪里找到这些功能?在现有代码中:只需查看 C++ 代码通常手动编写以表达如何执行常见操作的模式和习语,并设计一种直接表达他们想要完成什么意图的方式。(这与著名归因于威廉·H·怀特的校园设计策略相同:先建造建筑物,但不建造路径,然后等待观察人们在草坪上走出的路径在哪里,然后铺设这些路径。我们可以在演进编程语言中做同样的事情。)
示例:这包括我们过去最受欢迎的成功案例,例如 range-for,它不仅高效,而且在构造时就是边界安全的(例如,使用 range-for 访问集合时,很容易提升边界检查,它直接表达“访问此范围内的每个元素”,而对于等效的 C++98 风格循环,则很难优化)。同样,<=>
是添加到语言中的第一个使标准本身更小、更简单的功能,因为它删除的库条款中的样板页数比它在语言条款中添加的文本还要多。类似地:Lambda 函数、constexpr
函数等等。
示例: constexpr
函数直接表达意图,并且比等效的模板元编程更简单、更安全、构建速度更快,模板元编程滥用模板类型系统作为一种偶然的图灵完备函数式语言来间接表达计算,而它从未被设计用于此目的。
4.4 可采纳性:避免病毒式注解
示例,“病毒式向下传播”:我们应该避免“我不能在这个函数/类上使用它,除非我首先在它使用的所有函数/类上使用它”的要求。这将需要自下而上的采纳,并且在任何语言中都很难大规模采纳。例如,我们应该避免要求安全或纯函数注解,其语义是安全或纯函数只能调用其他安全或纯函数。我们已经有了 constexpr;我们添加得越多,人们将需要编写的组合式修饰符就越多,尤其是在较低级别的库中,我们应该谨慎对待任何新修饰符的价值。
示例,“病毒式向上”:我们应该避免“我不能在这个函数/类上使用它,除非它使用的所有函数/类都要求添加它”的要求。例如,Java 的受检异常注解要求列出函数可能抛出的类型(另请参阅 3.5),这为该函数的每个调用者创建了可用性障碍,要求它们也将这些列表包含在其受检异常类型列表中;实际上,该功能并未被广泛使用,程序员通过在调用者上编写 throws Exception 来有效选择退出并禁用它。
4.5 可采纳性:避免重型注解
“重型”指“每 1000 行代码多于 1 个注解”之类的意思。即使它们提供了显著优势,这种基于注解的系统也从未成功地大规模采纳过,除了有时在公司内部受到自上而下的行政命令强制(例如,微软内部要求使用 SAL 注解;但外部客户并未大规模采纳 SAL)。
4.6 避免默认情况下泄露实现细节的功能
这破坏了封装,而封装绝不应是默认的或容易意外发生的。它会创建依赖项,使代码更脆弱、更难维护。
示例:Java 的受检异常(再次)通过列出可能抛出的异常类型来暴露实现细节,这些类型在代码的当前级别可能没有意义(如果它们源自更低级别)。
示例: [P3294R1] 源代码(token)生成默认生成真实的源代码,因此它不会绕过一些语言规则(例如,可访问性检查),这意味着会生成一种不同的语言而不是程序员可以手动编写的 C++ 代码。另请参阅 [P3437R0]。 [P2996R5] 将“拼接”作为一种代码生成形式,它确实绕过了访问控制(就像成员指针已经做的那样),只要它不是默认的并且不会意外使用,那就没问题。
4.7 当 `consteval` 库能够拥有与内置语言功能同等可用性、表达性和性能时,优先选择它们。
这能够实现更快的迭代和更高质量的功能,因为您不必成为编译器编写者(或等待编译器更新)即可拥有该功能,并且在实际 C++ 代码中实现更容易测试和调试。
示例:如果一个功能可以通过 consteval
库代码使用编译时函数、反射和/或生成来实现,并具有类似的可用性、表达性和性能,我们就不应将其添加到语言中。
5 参考文献
[D&E] B. Stroustrup. 《C++ 的设计与演进》(Addison-Wesley, 1994)。
[P2996R5] W. Childers, P. Dimov, D. Katz, B. Revzin, A. Sutton, F. Vali, D. Vandevoorde. “Reflection for C++26”(WG21 论文,2024 年 8 月)。
[P3294R1] A. Alexandrescu, B. Revzin, D. Vandevoorde. “Code injection with token sequences”(WG21 论文,2024 年 7 月)。
[P3437R0] H. Sutter. “Proposed default principles: Reflect C++, generate C++”(WG21 论文,2024 年 10 月)。
[HOPL-II] B. Stroustrup. “A History of C++: 1979-1991”(ACM 编程语言历史会议 II,1993 年 4 月)。
[HOPL-III] B. Stroustrup. “Evolving a language in and for the real world: C++ 1991-2006”(ACM 编程语言历史会议 III,2007 年 6 月)。
[HOPL-IV] B. Stroustrup:“在拥挤和变化的世界中蓬勃发展:C++ 2006-2020”(ACM 编程语言历史会议 IV,2020 年 6 月)。