为什么美国F-35战斗机禁止90%的C++特性
一次未处理的异常在几秒内摧毁了价值5亿美元的火箭。
F-35绝不会重蹈覆辙。
工程师们通过精雕细琢C++语言,创造了史上最严苛的编码规范之一。
这便是——战斗机飞行员式的编程之道。
本视频将带您深入探索航空软件发展史,揭秘五角大楼曾需应对的(数千种!)编程语言,以及其中最狂野的存在——联合攻击战斗机(F-35)的C++编程标准。
《联合攻击战斗机(JSF[F-35])航空器C++编码标准》概述
文档属性
- 文件编号:2RDU00001 Rev C
- 发布日期:2005年12月
- 版权方:洛克希德·马丁公司
- 分发权限:公开分发,无限制
文档目的与定位
本编码标准旨在为C程序员提供明确的指导和规范,以确保在系统开发与演示(SDD)项目中编写的代码具有高安全性、可靠性、可测试性与可维护性。标准结合了汽车行业软件可靠性协会(MISRA)的C语言安全指南、车辆系统安全关键C编码标准,以及C语言特有的安全编程原则,适用于航空器(AV)安全关键代码开发。
核心内容结构
- 通用设计原则
- 强调高内聚、低耦合的模块化设计
- 限制函数长度(≤200行)和圈复杂度(≤20)
- 禁止自修改代码
- C++编码标准细则
- 规则类型:分“应遵循”(should)、“将遵循”(will)、“须遵循”(shall)三级,逐级严格
- 环境要求:代码必须符合ISO/IEC 14882:2002标准,限制字符集使用,禁用三字符组、双字符组等
- 库与预处理:仅允许使用DO-178B A级认证库或SEAL 1级库;限制预处理指令(仅允许
#ifndef、#define、#endif、#include) - 命名与风格:统一命名规则(如类名首字母大写、变量全小写、常量用
const而非#define)、缩进、括号风格等 - 类设计:强调接口完整性、禁用公有数据成员、显式禁止不需要的隐式成员函数、正确管理资源生命周期
- 继承与多态:推荐基于抽象类的层次设计,限制多重继承形式,遵循里氏替换原则(LSP)
- 模板与泛型:要求模板参数约束检查,避免过度依赖实例化上下文
- 函数设计:限制参数数量(≤7)、禁止递归、避免返回局部对象指针/引用
- 注释与文档:仅使用C++风格注释(
//),删除无用代码,注释应解释逻辑而非重复代码 - 类型与转换:使用明确长度的类型别名(如
int32),避免隐式类型转换,禁用C风格强制转换 - 内存与指针:禁止初始化后从堆中动态分配内存,限制指针间接层级(≤2),避免裸指针算术
- 错误处理:禁止使用C++异常(
throw/catch) - 可移植性:避免依赖硬件特性(如字节序、内存布局)
- 效率与优化:反对过早优化,优先保证代码清晰性与安全性
- 测试要求
- 派生类必须通过基类的所有测试
- 结构覆盖率分析应在“扁平化”的类层次上进行
- 对含虚函数的继承层次,需测试所有多态路径
- 附录支持
- 附录A:详细解释关键规则,提供代码示例与原理说明
- 附录B:说明可通过LDRA工具自动检查的规则,其余需人工审查
核心理念
- 安全优先:通过限制“危险”语言特性(如指针算术、动态转换、异常),定义安全的C++子集
- 防御性编程:强调运行时检查与错误处理,确保系统在异常条件下的稳定性
- 代码即文档:通过一致的命名、结构化和注释,提升代码的可读性与可维护性
- 工具辅助合规:鼓励使用静态分析工具(如LDRA)自动检查规则,减少人为错误
典型规则示例
AV Rule 29:用inline函数替代#define宏AV Rule 76:含指针或非平凡析构函数的类必须定义拷贝构造和赋值操作符AV Rule 119:禁止递归AV Rule 208:禁用C++异常AV Rule 215:禁止指针算术,推荐使用容器类
适用对象
- 强制适用:航空器(AV)安全关键C++代码开发
- 推荐适用:非航空器C++开发项目
该标准体现了在高可靠性嵌入式系统(如战斗机航空电子软件)中,对代码质量、安全性和可维护性的极致要求。


http://journal.th…(视频中至少间接提及)是工程主管撰写的一篇优秀文章,深入探讨了“为何选择C++”。简而言之:“他们找不到足够的人来编写Ada程序,即便能找到,也缺乏足够的Ada中间件和工具链。”
我认为如今推广Ada语言会比当年容易得多。软件领域整体上已更开放地接纳多样化的语言与概念,掌握Ada不会像过去那样被普遍视为职业发展的局限。况且Ada正迎来复兴,例如英伟达采用SPARK语言。
我始终强烈反对“缺乏X语言程序员”的论调。若国防部强制推行Ada标准,高校、职业培训机构和企业必将跟进。人们完全能掌握新语言。若当年F35战斗机采用Ada而非C++,美国的战备状态本可更完善。
我认同“X语言程序员短缺”的论调普遍站不住脚。真正适用该论点的情境仅限于维护严重过时或即将淘汰的平台这类特殊领域——COBOL就是典型例子。
但并非因为我认为学校会因某个政府部门或大型企业支持/强制推行就立即行动,开始培养下一批该语言人才。主要原因是我记忆所及,这种情况从未真正发生过。相信我,我多么希望学校能如此积极主动或与行业需求保持同步,但……(耸肩!)
正如我所说,我依然认为最初的论点存在缺陷——至少在普遍情况下如此。任何优秀的企业都不该只招聘“X语言”程序员,而应招募能将问题解决能力迁移到各类语言体系中的优秀程序员。培养优秀程序员掌握新语言所需的投入,远没有多数企业宣称的那么昂贵。
不过,若你执意选择某些_极其冷门_(即“问题重重”)的编程语言,无论哪种途径都难以获得有效支持,所以……(耸肩!)
> 若国防部强制推行Ada语言要求,高校、职业培训机构及企业必将跟进
国防部确实强制推行过Ada要求,但高校等机构并未跟进。
联合攻击战斗机(JSF)的C++指南正是为规避国防部Ada强制令而制定(视频中已讨论过)。
不会跟进。国防部在软件市场中的份额微不足道。采用商用现成软件比定制解决方案能获得更高质量和更低成本,除非你愿意投入 巨额资金 。软件劳动力市场亦是如此。
人们热衷贬低C++,因为它(a)流行且(b)试图通过内置大量不同范式取悦所有人。但用它编程几乎能实现任何系统的可扩展性,远胜其他语言。
据我观察,人们批评C多是针对其安全性问题。但安全性在不同领域的重要性本就存在差异。我并不认为C能比Ada带来更优的代码质量。
赞同。首先我认为Ada并非难学语言。不妨先雇佣C++程序员,再让他们学习Ada。
其次,当企业声称“我们招不到足够的X”时,真实含义是“X太贵了”。他们可能设有严格的薪资区间,而无人有权突破限制。
换言之,优秀的Ada和C程序员虽昂贵却不缺,而廉价的垃圾C程序员却比比皆是。
实际上这类项目长期超支,而美国军方以挥霍资金闻名。
相较于幻想建立Ada生态系统,采用C++或许是少数成功的成本节约措施之一。
需注意这些并非普通程序员,他们必须持有安全许可并满足特定资质要求。
他们必须满足极其严格的安全审查标准,并在项目周期或任职期间持续保持。人们往往忽视这并非在ESP32上随便搞个嵌入式小程序那么简单。
你将接受全面审查:家人、邻居、学校老师、前任上司、远房表亲、警长、旧情人,甚至儿时玩伴都将被调查。你的生活将被放大镜审视。
我经历过TS级安全审查流程(虽是信号情报领域而非战斗机软件开发,但流程应大同小异)。
如果我重返就业市场,我会要求高额补偿才肯再经历一次。这个过程极具侵入性,严重限制了你的行动自由,还增加了巨大的就业不确定性(因为你的工作现在与安全许可捆绑在一起)。
更不用说嵌入式软件的薪资往往只有初创公司的一半,而国防软件通常不支持远程办公。别问他们能招聘哪些编程语言人才。他们靠工作本身的吸引力来弥补大幅降低的薪资和恶劣的工作环境。再加上部分从业者对该领域存在道德顾虑,可见他们只能招到三类员工:找不到其他工作的、想在简历上添点炫酷经历的、以及真心热爱这个领域的人。而当这批人成为团队核心成员时,企业就会失去中间那类人才——对他们而言这始终只是跳板。
没错,但就像专业认证,安全许可属于个人而非企业。它能随你跳槽,且有效期很长。只要持有该许可,诺斯罗普、洛克希德、波音等政府承包商都会争相聘用。
工程学位加机密级安全许可基本等于铁饭碗。虽非FAANG那般光鲜,但就业保障性极强。当前经济低迷时期,大城市有人抱怨数年找不到工作,而我本地洛克希德、BAE、博思艾伦等公司仍在招聘。
我的问题在于,你最终要应付那些不愿学习、只想榨取薪资和工作保障的蠢货,当你试图改进时他们还会主动对抗。这种现象已制度化。
正如我写给别人的:
为何要求企业使用特定编程语言,而非要求最终产品品质优良?> 若F35采用Ada而非C++,美国的战备状态如今本该更好。
此论据何在?销售Ada产品的公司必然赞同,毕竟他们利益相关。Ada并不能自动带来更优质、更稳健、更安全或完全无误的软件。
你的论点既危险又不诚实,现实案例已令人遗憾地证明了这一点[0]
[0]: https://en.wikipedia.org/wiki/Ariane_flight_V88
> 此次事故被公认为史上最臭名昭著且代价最惨重的软件缺陷之一[2],造成超过3.7亿美元损失[3]。
> 发射失败事件使公众、政界及企业高管意识到复杂计算系统的重大风险,从而推动了保障安全关键系统可靠性的研究。随后对阿丽亚娜代码(采用Ada语言编写)的自动化分析,成为抽象解释法实现大规模静态代码分析的首个案例[9]。
Ada语言(尤其是Spark子集)极大简化了正确软件的开发过程。但这并不意味着它能自动生成更优质的软件——编程语言仅是整个开发体系中的关键环节之一。
> Ada(尤其是Spark)极大简化了正确软件的开发过程。
相对于什么而言?其他语言也有形式化验证工具。我听说Ada/SPARK很优秀,但无法证实其真实性。况且推广Ada的公司本身就是利益相关方。
况且Ada也未能阻止阿丽亚娜5号的Ada代码酿成灾难。
> 编程语言只是整个拼图中的一小块,但却是重要的一块。
完全正确,但楼主认同的前文观点是:
> 若F35采用Ada而非C++,美国的作战准备状态如今会更好。
有何证据佐证?尤其考虑到阿丽亚娜5号这类事件?
况且相较于其他语言,Ada在技术与非技术层面都存在争议性缺陷。
数周前我尝试用Ada编写微型示例时,就发现某些方面相当繁琐。其语法是否比C++更冗长臃肿?不过这或许只是学习曲线问题。即便强制推行,Ada也未能普及。
>有何证据支持此论点?尤其考虑到阿丽亚娜5号火箭事件?
阿丽亚娜5号虽是反Ada的经典论据,但Ada实为美国军用机器最广泛使用的语言。
现在争论的焦点或许是美军是否优于X军;但世界上最大的军队里充斥着运行Ada代码的战争机器,这本身就证明了该语言/国防部/资助体系的有效性。
用C++会更好吗?我无法断言,但否认Ada的成功实属荒谬。
但Ada语言曾有多年强制使用令[0]。这本应是极强的竞争优势。即便如此,C至今仍在某些美国军事项目中使用,比如F-35战机。虽然我不知道F-35是否成功,但若失败,这或许能成为反对C的论据。
在小众领域之外,Ada几乎销声匿迹。
主要倡导Ada的公司似乎都是提供Ada服务的企业,这意味着他们本身就是利益相关方。
我几乎没有Ada编程经验,主要印象是它和C++一样历史悠久。
[0]: https://www.militaryaerospace.com/communications/article/167…
> 国防部计算机主管小埃米特·佩奇建议撤销国防部强制要求在实时关键任务武器及信息系统中使用Ada编程语言的规定。
为何要求企业使用特定编程语言,而非确保最终产品品质优良?
> 若F35战机采用Ada而非C++开发,美国的作战准备状态本可更胜一筹。
此论据何在?销售Ada产品的企业必然附和,毕竟他们利益相关。Ada语言本身并不能自动造就更优质、更稳健、更安全或完全无误的软件。
你的论点危险且不诚实,现实案例已令人遗憾地证明了这一点[0]
[0]: https://en.wikipedia.org/wiki/Ariane_flight_V88
> 此次事故被公认为史上最臭名昭著且代价最惨重的软件缺陷之一[2],造成超过3.7亿美元损失[3]。
> 发射失败事件使公众、政界及企业高管意识到复杂计算系统的巨大风险,从而推动了保障安全关键系统可靠性的研究投入。随后对阿丽亚娜代码(采用Ada语言编写)的自动化分析,成为抽象解释法实现大规模静态代码分析的首个案例[9]。
> 为何要求企业使用特定编程语言,而非确保最终产品品质?
我认为有两个原因。首先,使用更优语言实现同等正确性可能成本更低。其次,必须承认测试本身也无法做到百分百正确和完整。我认为从更优基准出发只会带来益处。
不过我从未使用过C或C++的正式验证工具。或许它们能弥补语言本身的缺陷。
如何定义更优的编程语言?如何评判语言优劣?又如何防止腐败与垄断集团操控市场?
若Ada真比C++“更优”,为何其在安全性与正确性(如Ariane 5火箭案例)及商业应用(细分领域与整体市场)方面均未显著超越C++?众多企业本可借助“更优”语言获得巨大竞争优势。为何自由市场未选择Ada?
或许有人辩称C++拥有免费编译器,但Ada强制推广政策本应在某种程度上抵消这一优势。企业为何不采用Ada?
Rust的普及度远超Ada,至少在Ada的细分领域之外如此。部分原因源于技术优势,例如Rust出色的模式匹配、模块与仓库设计;部分则属人为推动,比如Rust传教士通过强制手段、威胁[0]、骚扰[1]及有组织的付费媒体轰炸来推广该语言。
我曾尝试用Ada编写小型示例程序,某些方面甚至感觉不如C++。不过当时只投入了几个小时左右。
[0]: https://github.com/microsoft/typescript-go/discussions/411#d…
[1]: https://lkml.org/lkml/2025/2/6/1292
> 技术补丁和讨论很重要。社交媒体围攻——谢了,不干。
> 林纳斯
https://archive.md/uLiWX
https://archive.md/rESxe
你所提议的恰恰相反的情况已经发生:Ada曾被强制采用,随后该强制令被撤销。成为特定产品的唯一客户通常是个糟糕的主意,因为这会推高成本。
> 若采用Ada而非C++,F35战机和美国的战备状态如今本应更佳
F35战机和战备状态有什么问题?许多欧盟国家正争先恐后地购买它。
> 许多欧盟国家争先恐后地购买它
他们购买该战机并非看重其性能,而是为了取悦美国这个盟友/霸凌者——否则美方必将实施经济报复。
看看最近瑞士的案例:该国飞行员本已选定另一款战机(法国阵风),却在事后遭到本国政界的否决。
现有欧洲F-35机队大多购于特朗普首任总统任期之前。事实上如今趋势恰恰相反:各国正从可靠伙伴处寻求替代方案,即便技术稍逊。
巴基斯坦疑似在200公里外击落三架战机后,飞行员们可能重新评估了该机型。虽归咎于情报失误,但实际存在多重因素,其中部分或可归因于战机本身。
或许欧盟本就不该沦为美国的附庸。
弱者永不得尊重,纵使盟友亦然。讽刺的是,展现骨气并在某些议题上与美国脱钩,短期虽会更痛苦,但长远更健康。
>或许欧盟本就不该沦为美国的附庸。
我完全赞同。若你(名义上)是全球最大经济体,却如此轻易被欺凌,那早在二十多年前就已失败。
但我认为这并非欺凌,恰恰相反。欧盟国家不过是用金钱换取美国军事保护的优待,毕竟这远比撕掉创可贴、自建同等规模军工产业划算得多。
多数国防开支都基于相同动机:你追求的并非最优或最廉价的装备,而是购买强大的盟友。
F-35项目超支约10年工期和500亿美元。
例如英国希望在F-35战机上搭载本国研制的空对地导弹(矛导弹),却因洛克希德·马丁公司Block 4软件升级延误而受阻。
> F-35战机的作战准备状态存在什么问题?
首先Block 4升级严重延误。
> 许多欧盟国家争先恐后地采购它。
因为我们天生渴望更多自由。
既然仍有7家厂商在销售Ada编译器,我始终觉得这种论调有些虚伪。
https://www.adacore.com/
https://www.ghs.com/products/ada_optimizing_compilers.html
https://www.ptc.com/en/products/developer-tools/apexada
https://www.ddci.com/solutions/products/ddci-developer-suite…
http://www.irvine.com/tech.html
http://www.ocsystems.com/w/index.php/OCS:PowerAda
http://www.rrsoftware.com/html/prodinf/janus95/j-ada95.htm
事实是,这些供应商以及其他许多供应商(如曾经提供Ada编译器的UNIX供应商,例如Sun)都将Ada编译器视为额外收费项目,而C和C++编译器早已包含在UNIX开发者套件中(这一传统由Sun开创,其提供多种UNIX套件)。
因此,学校和许多开发者发现购买C或C++编译器比购买价格高昂的Ada编译器更为便捷。
Ada Core的卓越贡献对Ada发展起到了积极作用,尽管有人对他们爱恨交织。他们不仅是ISO工作的主要赞助方,还在开源社区积极推广Ada知识。
Ada未能普及的另一因素或许在于:https://en.wikipedia.org/wiki/Ariane_flight_V88
> 此次失败被公认为史上最臭名昭著且代价最惨重的软件缺陷之一[2] 此次事故造成超过3.7亿美元损失[3]
> 发射失败事件使公众、政界及企业高管意识到复杂计算系统的重大风险,从而推动了保障安全关键系统可靠性的研究。后续对阿丽亚娜火箭代码(采用Ada语言编写)的自动化分析,成为抽象解释法实现大规模静态代码分析的首个案例。[9]
阿丽亚娜号的失败并非Ada语言特有。
这仅仅说明:无论使用Rust还是其他所谓更安全的编程语言,任何语言都可能编写出垃圾程序。
若用C语言编写程序并启用溢出错误捕获选项进行编译,其行为将与阿丽亚娜号的Ada程序完全一致。
一个忽略异常的程序本会继续运行,但火箭很可能稍后仍会因程序的荒谬决策而坠毁,且故障原因将更难追溯。
人们总爱强调这点,却忽视了C衍生语言中大量存在的故障。
但C衍生语言的使用范围更广。这恰恰说明Ada并不能自动使软件正确可靠。这种情况或许确实导致Ada不如预期那样普及。
系着安全带的人仍会死于车祸,因此安全带毫无用处。
但这并非我的论点所在。讽刺的是,这反而更适用于你前文提出的论据。
更合理的论证应基于统计数据。但这既难以实施,统计数据又极易被操纵且难以正确处理。
我认为企业应自由选择任何可行方案,同时确保流程与最终产品符合质量要求。强制使用Ada或其他特定编程语言,似乎既无法避免阿丽亚娜5号事故,也不可能提升安全性、可靠性或正确性,反而会为限制竞争、形成垄断及制造虚假安全感打开大门。我认为责任绝不应推诿给编程语言本身,程序员、组织及企业才应为其语言选择及使用方式(例如采用形式验证子集)承担责任。另一方面,制定类似ISO 26262和ASIL-D的标准与资质认证(正如Ferrocene公司为Rust语言产品所做的努力)是可取的。尽管具体而言,Ferrocene衍生规范中的某些内容显得相当离谱。
我认为你描述的做法是可行的,但尚未听说有编译器实现此机制。
采用这种方式能获得什么优势?
此外,在你的结构中如何处理嵌套函数调用?虽然肯定存在复杂的实现方案,但基于当前调用假设尚不明确。
此举还会导致大量ABI兼容性问题。
无论如何,我主要在Risc-v和ARM平台编程——多数编译器倾向于通过寄存器传递参数,但仍会使用栈存储局部上下文。
在x86架构上同样可行,只需用jmp替代call指令,并设计自定义寄存器方案。这个x86程序完全不使用栈:https://github.com/jcalvinowens/asmhttpd
我认为实现这种编译器并不困难,尽管其功能必然受限(如你所言无法支持嵌套调用,或者说只能在可浪费的寄存器数量范围内实现…)。
> 我认为软件领域整体上已更开放地接纳多样化的语言与概念,如今掌握Ada语言不会像过去那样被普遍视为职业发展的局限。
你确定吗?我甚至在[0]中都找不到Ada的踪迹。
几周前我尝试修改Ada语言的Hello World示例,不得不说对语法并不满意。虽然某些特性设计精巧,但在构建和文件组织方面遇到困难——类似C++(但不同于Rust)存在多种源文件类型,比如C++的头文件机制。此外还遇到部分编译选项问题,不过当时尝试的是实验性功能,这部分责任在我。
[0]: https://redmonk.com/sogrady/2025/06/18/language-rankings-1-2…
是啊,我也盼着它能重振雄风。
我肯定在理想化它,但至少不像当年那些人那样妖魔化它。
运行许多卫星的软件也是如此。禁止使用STL。
核心问题在于任务保障。使用栈或堆意味着变量地址不固定。若特定存储单元损坏,这将引发严重问题。若所有变量拥有固定地址,当某个地址失效时,可通过加载补丁迁移该地址,使任务得以继续。
> 使用栈或堆意味着变量地址不固定。
你提及STL似乎在讨论C++。但据我所知,没有任何C++编译器能让你完全避免栈的使用——即便禁用常见功能(如RTTI和异常处理)。当然,你需要避免在函数体内(或块作用域内)定义局部变量,但这远远不够。
* 编译器需要为每个函数的参数和返回地址进行静态空间分配。早期编译器确实如此工作,但如今这会极度低效——程序二进制文件中定义的函数数量远超任何时刻实际执行的函数量。(编辑:考虑到函数代码本身已占用空间,为参数分配空间或许影响不大。)
* 递归将无法实现,包括相互递归(需依赖运行时检查,因编译/链接阶段难以检测),虽然实际影响可能比听起来小,但据我所知尚无C++编译器支持此特性。
* 还需完全避免创建临时变量,例如当 a,b,c 为非平凡类型时,y = a + b + c 将被禁止。(y = a + b 则可行,因临时变量可直接构造到 y 的内存区域,或临时存储在相关运算符+()的返回空间中——该空间同样为静态分配)。
这真的是你的本意吗?我怀疑并非如此,但若不作此假设,你关于避免栈使用的论点便毫无意义。
你的观点没错,但递归在安全关键型应用中本就禁止使用。核心问题在于确定性。你必须使用栈来存储调用栈这一事实是正确的,楼主似乎存在认知偏差。
在x86/x86-64处理器上,程序调用必须使用栈——硬件强制要求如此。
在其他主流CPU指令集架构中,返回地址存储于寄存器,编译器可轻松实现仅使用寄存器传递的函数参数。其代价是函数参数数量存在合理上限(如12或24个),具体取决于通用寄存器数量(如16或32个)。若极少数情况下程序员需要更多参数,则应将部分参数组合为结构体。
采用此规范通常不会引发问题,且无需调用栈。可使用软件管理的栈,甚至在需要时实现递归。
仅在早期计算机中,因寄存器资源匮乏,才需要使用静态内存传递函数参数。
> 若特定存储单元损坏则后果严重。当所有变量拥有固定地址时,若某地址失效,可通过加载补丁移动该地址使任务继续执行。
这种处理方式显得过于手动,其实可设计自动化方案。例如采用特殊ECC内存,通过里德-所罗门编码应对整单元故障,或在启动过程中建立坏单元黑名单等。
远不止于此。
这正是远程调试得以实现的关键。在超低带宽链路上进行交互式远程调试是不可能的。若所有组件均采用静态地址和确定性静态配置,便可在地面构建精确副本进行调试。
交互式调试显然可行,据称深空一号任务就实现过。参与开发的工程师之一我记得常在HN发帖。
算是吧。但耗时数小时而非数天。
本地精确复刻加确定性状态能节省大量时间。
> 使用栈或堆意味着变量地址并非固定
那该把变量放在哪里?作为全局变量?又如何检测内存单元损坏?
程序拥有数据段。它既非堆也非栈…且可作为可靠的对象-地址映射源(取决于加载器/链接器),因其并非动态填充。
听起来你的答案是:“是的,全局变量”。
在许多嵌入式环境中这可能是绝佳方案,但在其他多数场景下全局变量被视为不良设计,或因限制性强而难以实用。
> 全局变量被视为不良设计
指的是全局 可变 变量,且通常会归类为单例(解决初始化问题,且更少引发争议)
它们被认为不实用,主要是因为语言工具链缺乏相应支持。
能否详细说明?
例如,更好的工具链如何帮助将TCP缓冲区存储在全局内存中?
快速举例:比较在嵌入式系统中使用C语言静态uint8_t[MAX_BUFFER_SIZE]配合FreeRTOS信号量和写入字节计数器,与使用Rust的
heapless::Vec<u8, MAX_BUFFER_SIZE>并通过大使馆互斥锁(embassy Mutex)保护的方案。前者会相当麻烦,因为需要管理三个全局变量;而后者则类似在普通操作系统上运行的多线程Rust代码,只是需要额外逻辑来处理缓冲区过度增长的情况。
你或许能从C代码中榨取更多性能,尤其当你深入了解系统时,但(根据经验)很容易迷失程序状态,最终自食其果。
栈上绝对可以存放局部变量。不知你从何得出“不能使用栈”的结论。虽然内存池被广泛采用,但堆内存基本属于禁忌。
> 若每个变量都有固定地址,当某个地址损坏时,可加载补丁移动该地址,任务仍可继续。
你完全可以将栈区和堆池固定在特定内存范围,这种操作始终可行。但这种推理逻辑我完全无法认同。
所以你实现的函数都不带输入/输出参数?
如果参数很少,或许能全部存入寄存器(不确定他们用的是什么架构?)
这不能在运行时实现吗?比如底层调用可将硬件地址列入读写故障黑名单?
若有闲置内存且使用带MMU的硬件,可将逻辑地址重映射到其他页面。Linux支持此功能,但仅限用户内存。
这假设操作系统能正常运行。若内存损坏影响到操作系统,可能就无法恢复了。随着系统(和软件)日益复杂,保持这些任务保障最佳实践变得愈发重要,但当代开发者有时会忽视这一点。
一个典型案例发生在约15年前我曾参与的项目中。项目负责人试图将繁琐细节从地面用户端抽象化,让用户只需向航天器“登记意图”,系统便自动执行操作。他同时要求移除“内存转储”等关键异常处理功能。若当时我在该团队,定会强烈反对,但实际情况是,我需要那位项目负责人作为盟友。
>此假设基于操作系统能正常运行。
下载使用不同内存地址的新版软件同样需要运行系统。关键在于:若能修补软件,就能修补内存映射。
>这假设操作系统能正常运行。
可将两个操作系统副本映射到不同内存区域。CPU启动时加载首个副本,若失败则看门狗触发,CPU可尝试启动第二个副本。
哦,需要澄清的是:若需达到该可靠性级别,我不会采用此方案。
或者我可能会使用MMU,但通过传统方式编写的内核驱动它——这种内核不进行内存分配。具体取决于可用硬件及需要容错的故障类型。
(我并非航空航天软件开发人员。)
哇,但他们如何处理异常情况?
我的意思是,即便代码库触手可及且可测试,我依然不认为测试足够?在C/嵌入式项目中,我常能发现被遗忘的边界情况和各类漏洞——正因为我运行程序、调试程序,才能发现内存问题及其他诸多需要尽可能收集信息才能解决的状况?
> 所有if、else if结构都应包含最终else子句,或添加注释说明为何无需最终else子句。
我同样采用此做法,但额外会输出日志提示:“值既未被找到也未被判定为不存在。此情况绝不应发生。”
这对调试极具价值。当代码大规模运行时,非零概率事件持续发生,即使无法理解原因,能立即掌握事件本质对我而言意义重大。
我喜欢Rust的匹配机制正是因为:它强制覆盖所有分支。
事实上,若能明确覆盖所有情况,不使用默认分支(else子句的等效形式)才是理想方案。因为当可能性扩展时(例如枚举新增值),编译器会强制要求覆盖新情况,否则可能遗漏。
而我喜欢在C中使用枚举 😉 编译器会提醒你覆盖所有分支。
https://godbolt.org/z/bY1P9Kx7n
Rust比这更智能,它不仅在枚举中,更在所有可能状态的穷举性上做到覆盖:
fn g(x: u8) { match x { 0..=10 => {}, 20..=200 => {},
}
例如上述代码会报错,指出未覆盖11至19及201至255的区间。
虽然可尝试将区间映射到枚举值,但映射过程中无法保证完整覆盖所有区间,这只是将问题转移到了其他地方。
Rust的方法并非完美无缺,对于i32或浮点数这类较大的数据类型无法实现全覆盖检查(我猜是出于性能考虑),但依然相当实用。
编译器还会提醒你:即使覆盖了枚举的所有成员,仍需添加`default`来覆盖所有情况,因为C语言枚举允许非成员值。
同感。我更进一步创建了宏
_STOP,其定义等同于你语言中的 DebugBreak()。若问题极其严重,则使用_CRASH(这会迫使我立即修复问题)“标准”定义(常见于我熟悉的项目规范,且C23标准已正式采纳)是“不可达”:https://en.cppreference.com/w/c/program/unreachable.html
这完全不是同一回事。“不可达”意味着整个分支无法被执行,编译器可据此自由注入优化。即使未触发违规,程序也不必崩溃——实际上通常不会崩溃。这相当于以下代码:
这种写法曾直接导致Linux内核的安全漏洞——解引用空指针属于未定义行为(即便在工程师认为其语义明确的内核中),而该代码省略了空指针检查,从而形成漏洞。
我认为在关键任务软件中使用unreachable()极其危险,其风险远高于内存分配失败。应彻底消除所有潜在的未定义行为(即采用安全Rust模式,尽可能避免或最小化不安全操作,而非将未定义行为作为注释手段)。
你说得对,我之前链接的内容正是如此操作。我本该更仔细阅读。
我参与的项目中,都将其无条件定义为导致崩溃的行为(例如通过
std::abort并附带错误信息)。这些项目并未实际使用C/C++的实现(因C23标准过于新颖),且显然采用该实现是不正确的。你可以使用标准库的不可达代码检测器功能:https://godbolt.org/z/3hePd18Tn
对于多数项目类型和开发方法而言,规避未定义行为虽必要却远远不够。完全可能存在致命缺陷——即便不涉及任何未定义行为——仍会导致健康损失、生命危及或数百万美元的经济损失。
有趣的是,Rust的模式匹配——这项在无GC系统语言(如C、C++和Ada等小众语言)中的创新——在正确性和可靠性方面,可能比其著名的借用检查器更为关键。
PASCAL不是早就具备带原始模式匹配功能的变体记录类型了吗?
或许吧,我不确定。不过其后继语言Delphi似乎并未宣传自己具备模式匹配功能。
也许它过于原始,难以被视为现代意义上的模式匹配。几十年来模式匹配技术已有了显著演进。
C++越变越庞大了 😀
感谢分享
这其实是C语言的特性,据我所知C++从未实现过类似功能。
https://en.cppreference.com/w/cpp/utility/unreachable.html
C++23确实提供了std::unreachable(作为函数),以及对应的[[assume(expr)]]
所幸主流编译器厂商都已实现。绕开标准委员会的做法正变得越来越普遍。
感兴趣者可查阅F-35(原名联合攻击战斗机)的C++编码规范,共142页:
https://www.stroustrup.com/JSF-AV-rules.pdf
粗略浏览几页后,感觉规范相当合理。这让我好奇那些“应”规则的例外条款。如此规模的项目,这些例外应该能体现此类标准的实用价值。
正如硬实时代码的惯例,运行期间禁止动态分配:
当问题规模大致恒定时(如2005年情境),此机制运作良好。但现代AI引导无人机中情况如何?
现代环境为何会实质性改变此规则?初始化资源分配反映了硬件的局限性。预算就是既定值。
我无法想象“现代人工智能引导的无人机”会改变这种基本机制。某些系统能在固定分配约束下支持高度弹性且动态的工作负载。
基础飞行控制属于固定规模问题。如今更多军用飞机系统依赖环境与敌方动态信息运作。
你现在纯属在胡思乱想。
绝大多数嵌入式系统都围绕最大缓冲区大小和已知最坏情况执行时间进行设计。在这些系统中试图精细动态平衡资源,几乎总是错误的选择。
在句子中加入“现代”和“无人机”这两个词也改变不了这个事实。
当前环境中实体行为的实时追踪与分析,其计算瓶颈在于传感器分辨能力的限制。软件层面根本不存在因无人机等设备过量部署导致处理能力跟不上的情况。
这类系统固然存在极限,但其阈值极高。即便在极端情况下触及极限,那也属于优先级问题。几十年前当同时追踪上限仅数十个目标时,这类设计难题就已存在成熟解决方案。
某些导弹的内存分配速率按秒计算,其硬件内存仅够支撑导弹全程飞行加少量冗余。垃圾回收机制就是让导弹在目标处爆炸 😉
你实际操作是将分配逻辑从堆分配器转移到了程序逻辑中。
这样你就能使用大小精确可控的内存池或缓冲区。但除非程序始终保持恒定的内存占用,否则现在你必须自行管理池/缓冲区内的内存分配。
“AI”包含多种形态:专家系统、决策树、卷积神经网络、Transformer等。在多数推理场景中,模型是固定的,输入/输出结构预先定义,操作规则也已设定。因此本质上并不具备动态特性。
大型语言模型亦是如此。我实在不明白楼主的核心观点——AI(实际上所有机器学习)本质上都属于典型的“预分配简单”问题。
动态分配真正发挥价值的场景在于:为实现进程共存而需最小化峰值内存占用(例如存在其他运行进程),或通过利用代码模块间的时间差缩减物理内存需求(即组件A和B从不同时使用内存,故可复用同一内存区域)。动态分配还能简化某些算法,当处理可变长度输入时,它能避免设计阶段预估最大值的麻烦(前提是正确处理分配失败情况)。
你认为这些现代AI无人机如何运用其人工智能?具体在无人机哪个部分使用?
好奇它们是否通过静态分析强制执行这些规则,还是要求开发者自行掌握所有规范
“应”级建议会进行静态分析,“将”级则不会
静态分析
总体而言,这些建议对开发嵌入式或低规格设备软件是否适用?比如我根本不懂预处理器宏——读到这里时还觉得“没错,我同意…”直到看到禁止使用stdio.h!
更适用于嵌入式系统而非低规格设备。具体还取决于应用领域。
stdio.h 在某些嵌入式场景中可行,但在其他场景则绝对不可取
安全代码中不应使用 stdio.h。
他们用 f35io.h 吗?
视情况而定。硬实时系统会用厂商专属库、内部库或自定义函数。
据我所知他们大量使用Green Hills工具链相关组件。
初次见到这份文档时,有人将其作为例证——即便缺少诸多特性,为Arduino Uno编写的C依然是真正的C。
代码片段的字体选择很有意思。不知这是随性而为,还是刻意避开等宽字体的考量?
代码示例的字体与Bjarne Stroustrup所著《C++程序设计语言》(第三版/Wave出版社)几乎如出一辙。回想起来,他用 斜体 的可变宽度文本展示代码示例,却用制表符对齐注释,确实有点奇怪!
有趣的是他们选择了C++而非Ada。
视频深入探讨了军方最终接受C++而非强制推行Ada的历史缘由。
a = a; // Misra规范
我亲眼所见的真实代码(非F-35战机代码)。
这是为避免删除方法中未使用的参数而设计的。未用参数本应被禁止,但这种写法却被允许?
我怀疑这些编码规范能否真正造就优质代码!
已有研究探讨过MISRA规范,但据我所知尚未有针对JSF指南的研究。关于MISRA,研究结果参差不齐:部分规则似乎有效(合规软件缺陷更少),部分规则则适得其反(遵守这些规则的代码更易出现缺陷),还有些规则则无关紧要。
值得注意的是,这份文档发布于2005年。这意味着它诞生于C标准化之后,但又早于该标准的第二次修订,更比其作者Bjarne Stroustrup的重大转变整整早了二十年——这位多年坚称C方言是糟糕主意且绝不会被语言委员会采纳的专家,最终竟认定方言(现更名为“配置文件”)才是解决语言顽疾的灵丹妙药。
尽管劳里的视频很有趣,我同样对这类风格指南的价值持怀疑态度。诸如“应避免使用制表符”或“函数名字母须小写”之类的规定,并非源于某人飞机坠毁的荒诞事——纯粹是因为采用了Bjarne不喜欢的编程风格。
所谓“良性”规则如“勿写数组末尾元素”,而“恶性”规则如“禁止提前返回”或“变量名不得超过6字符”。95%的“良性”规则本质上只是“避免引发未定义行为”的冗长表述。
为何“禁止提前返回”不算好规则?
我编写的代码中会使用提前返回,但纯粹因为大家都这么做。我更倾向于将元素置于可预测的位置:变量置顶,返回语句置尾。更简洁?Delphi/Pascal风格。
提前返回能使代码更线性,减少条件语句/缩进层级,某些情况下还能提升运行速度。简而言之,它往往让代码更简洁。“禁止提前返回”本质上是“禁止goto”的软化版本。但遵循这些经验法则未必总能写出好代码。软件工程师应追求最佳代码质量,而非僵化地遵循毫无意义的规则。
这涉及个人偏好。若无法提升代码质量,切勿随意添加提前返回。但多数情况下,它确实能显著增强代码的可读性和可维护性。
对我而言关键在于缩进/作用域深度。因此我倾向于在开头设置带先决条件检查的提前退出点——这些后续无需处理的逻辑可让我直接从缩进层级“0”开始编写后续代码,真正的结果位于末尾。
> 为何“禁止提前返回”不是好规则?
它 或许 是条好准则。
但绝非好 规则 ——死板遵循只会催生为迎合规则而写的晦涩代码。
记得在学校和教授为此争论过,他坚持函数只能在末尾设置单个“return”语句。即便我反复尝试,也未能让他解释这种做法的价值所在,更不清楚如何能提升代码质量,因此很想听听你的见解?
“禁止提前返回”规则的产生源于特定语境下的合理性,尤其适用于1990年前的C语言和FORTRAN代码。它属于“结构化编程”范畴,与迪杰斯特拉1968年提出的《跳转语句有害论》同期兴起。此后该规则逐渐成为公认真理——即人们未经深究便盲目遵循的准则。
以该规则为例:函数可能先分配内存,执行操作,最后在代码块末尾释放内存。若存在第二个退出点,极易遗漏释放操作,从而引发间歇性内存泄漏。此类代码更难推导逻辑,漏洞也更难定位。
源自:
> 过早退出存在的问题在于清理语句可能无法执行。… 每个返回点都需执行清理操作,这种做法脆弱易错且极易引发缺陷。
https://en.wikipedia.org/wiki/Structured_programming#Early_r…
此刻约90%的读者会想:“但在 $现代语言 中这根本不适用。我们有GC、(x) {}代码块、try-finally语句,或是确定性终结机制等等。”
他们说得没错。在多数现代语言中确实如此。“禁止提前返回”规则不适用于Java、TypeScript、C#、Rust、Python等语言,因为这些语言专门设计了安全的提前返回机制。
元规则在于:某些规则在失去实用价值后仍被沿用。理解规则的初衷才能判断其适用场景。缺乏依据的规则只会增加判断难度。某些规则得以延续:如今我们基本不再使用 goto 指令,仅采用其结构化封装形式如if-else和foreach循环。
> 变量名长度不得超过6个字符
我的记忆可能有些模糊,但我不认为MISRA有这样的规定。C89/C90标准指出,外部标识符仅需确保前6个字符的唯一性[1],而MISRA则要求前31个字符的唯一性[2]。
[1] https://stackoverflow.com/questions/38035628/c-why-did-ansi-…
[2] https://stackoverflow.com/questions/19905944/why-must-the-fi…
Bjarne只是个普通人,他无法左右C++委员会的投票结果,更别说控制你我对编程风格的决策了。
将这些准则简化为风格指南本身就是错误的。我从未收到过“nit: 圈复杂度过高,且使用动态分配”这类指正。
若将C++配置文件限定为无语义影响且不改变代码生成(仅限子集),它们岂不比方言简单得多?功能更受限,但确实简单得多。
至少能避免大量陷阱,比如
vector<bool>这类设计。等等,你该不会是Rust传教士吧?像fasterthanlime那样收了钱?
因该账号在多个账户持续违反规范,现予以封禁。
你难道没发现Reddit上总有几个固定人马,通过不断贬低其他技术来推销某项技术?这类账号/评论的泛滥,反而促使包括我在内的人站出来反驳——因为他们描绘的现实往往失真,甚至完全错误。这种行为既有害又明显会引发口水战,按你对上述账号的处理原则,这难道不也违反了社区准则?
我们依据所见采取行动,而我们所见取决于用户通过举报和邮件提供的线索。
像你这样的评论难以处理,因为若不提供具体评论链接,我们既无法采取行动,也无法给出令你满意的回应。
编程语言争论在HN上向来乏味,当收到举报时我们绝不吝于对肇事者采取行动。
“无语义影响”是C领域反复出现的陈词滥调之一,如同“超集的子集”或“以性能换取安全性”这类说法——我认为即便其拥护者也该斥之为胡说八道。对属性“无语义影响”的执念严重损害了其价值,而Bjarne在C 20概念中刻意忽略语义含义的做法,更使之沦为本世纪初构想的“概念”特性的拙劣替代品。
至于我是否受雇宣传者,恐怕难以令你信服——记得参与OSM项目时曾获免费餐食,若再深挖,定能找出其他契机,只要你足够努力,就能将我“Rust是优秀语言”的观点解读为某种“报酬”。上周反种族主义抗议现场有位友善女士分发免费饼干,说不定她曾遇见过某家印刷Rust书籍的承包商员工?我感觉你家里可能挂着软木板,还缠着许多红丝带。
但这一切与鸟类法则有何关联?
在C语言中,不访问变量而引用它的正确/预期/标准方式是显式转换为void类型:
虽然存在常见的编译器扩展实现,但这是标准原生方式且始终有效。
若使用GCC则不然。
https://godbolt.org/z/zYdc9ej88
clang对此处理正确。
在GCC中确实能抑制未用变量警告。但函数调用似乎例外。
可使用 __attribute__((maybe_unused)) 或 [[maybe_unused]] 等形式(取决于规范版本?)来避免禁用整行错误。
有趣的是 [[nodiscard]] 居然有效!
而赋值给 std::ignore 两种情况都有效。
你为该函数添加了禁止忽略返回值的属性。明确禁用显式警告是否合理?
我需要某种明确方式告知编译器:我正在有意忽略结果。
我在尝试在失败路径中进行尽力而为的日志记录时遇到这个问题。我调用某个函数记录错误,但它可能失败。如果失败了,我究竟该怎么做?更彻底地记录日志吗?
是的。
当我的数据库日志记录失败时,我会写入一个记录数据库失败的文件(但不会写入原始日志文件)。
当文件日志记录失败时,根据具体应用场景,我会尝试其他方式传递该信息(即文件日志记录失败的事实)——无论是通过HTTP请求、电子邮件或其他途径。
数据库会崩溃,文件系统会填满。记录日志记录失败的情况至关重要。
而当最后一种方式也失效时,你该怎么办?
我倾向于设置独立监控进程监视主进程,并在不同数据中心部署专职监控机器。但最终主进程仍会执行日志记录尝试,检测失败后启动最终备份日志机制,并向监控器发送异常状态信号。它不会因最终备份日志的成败而改变决策逻辑。
我负责的系统不涉及生命安全,仅规划单层冗余机制。若数据库崩溃与本地文件系统满载同时发生,说明事态已恶化,届时必然出现大量其他异常征兆供我排查。
有时会。例如设置套接字的非关键选项时,若失败甚至不值得记录(可能因记录成本过高),此时只需忽略封装setsockopt的库函数返回值即可。
我(不幸地)在职业生涯中写过大量“安全关键”代码,编码规范总体上确实产生负面影响。真正让飞机不坠毁的是严谨的设计——这在实践中意味着故障安全机制、看门狗定时器、冗余设计,以及最重要的:不过度追求雄心勃勃的需求。
虽然可能有10%的规则合理,但这些合理规则往往显而易见,至少在嵌入式系统中属于基本要求(例如:别在可能根本没有完整libc的系统上尝试内存分配)。
许多编码规范规则与正确性无关,却完全关乎可读性与降低认知负荷(“此处该用哪种风格?”)
Zig语言通过以下写法明确体现:
由于未使用的变量会导致编译错误,这种情况相当常见:https://github.com/ziglang/zig/issues/335
这难道不会增加未使用变量滞留代码库的概率吗?当你进行实验时,代码无法编译,于是添加该变量(可能由自动工具生成),代码随即通过编译。实验成功令你欣喜,由于编译器未报错,你便提交代码,导致垃圾代码残留其中。
这种设计既阻碍实验推进,又使未使用变量滞留最终版本,难道不是糟糕的设计吗?
这确实是Zig设计中颇具争议的方面。我更倾向于将其设为警告。所谓“警告总会被忽略”的论点站不住脚——只要存在抑制机制,任何警告都可能被忽视。
最近有次访谈中,安德鲁似乎暗示(若我理解无误):Zig的未来方向是让所有编译(无论成功与否)都生成可执行文件。若存在语法或类型等严重错误,生成的程序会直接输出错误并返回非零值;而对于“未用参数”,编译器仍会生成预期程序,但返回非零值(这样就能被CI等工具捕获)。
为何编译器要这样做,而不是在编译时直接报错并退出?这样有什么好处?
这更像是调试/开发特性。你可以尝试某些想法而不必修改整个代码库。
Golang完全相同。
这种机制极其恼人,直到某天它突然发挥作用,阻止你执行意外操作时才显现价值。
我不明白警告机制为何不能实现相同效果,同时还能加快迭代速度。除非你和野蛮人共事——他们往仓库提交符合警告的代码,而你毫无纪律可言阻止他们。
> 我实在想不通警告为何不能达到同样效果,还能让你更快迭代。
在我接触过的几乎所有代码库中,当警告不被视为编译错误时,总会堆积数百条警告。因此最稳妥的做法就是将所有警告设为错误,强制人们修正。
> 除非你正与野蛮人共事——他们将仅符合警告标准的代码提交至仓库,且无人制止这种行为。
我曾与一位同事共事,他提交合并请求前从不编译/运行代码。我多次向经理反映此事(此前我已亲自告知其必须执行此步骤且这种行为不可接受),但经理始终未采取任何措施。
顺便说一句,这种情况比你想象的更常见。我读过一些PR,不得不拒绝它们,因为我读了代码发现根本行不通,所以我知道提交者根本没实际运行过代码。
我算是相当整洁的程序员,但人们连写个像样的提交信息都很困难,往往只写“修复了bug”这种空洞的描述。
Go语言有着强烈的个人风格。若你不喜欢K&R缩进规范,那也只能认了——其他任何写法都会报语法错误。
这倒像极了旧时代。
没错,但这种情况似乎更糟。它不仅阻碍实验,反而增加了未使用变量残留在最终版本的概率。我理解在格式风格上坚持立场能避免无休止争论,但这种选择对两个关键方面(实验效率和最终代码质量)的影响显然更负面。
若需保留未使用变量,直接赋值为_(下划线)即可。据我所知,gofmt(编辑器保存时应自动运行)会发出警告,但代码仍可编译通过。
这确实是种不同的思维方式,但让gofmt在提交前就挑刺,总比让编译器在运行时嚷嚷要好得多——这样能让你“边写边清理”,而不是先写出C++那种丑陋的代码团,运行后再花整天时间清理残局。至少对我来说是这样…
你没资格质疑Go开发者的智慧。他们将未用变量设为不可配置的硬性错误有充分理由,无需严格论证。
除非强制将警告转为编译错误(多数编译器支持此操作),否则开发者往往会忽略警告。我在TypeScript/C#代码库中工作,除非强制要求清理未使用的导入/使用声明和变量,否则人们会任其存在。
顺便说一句,这会导致依赖链问题,进而引发奇怪的编译错误。
那么未使用变量会引发什么意外后果?
规范并不能取代代码审查。实际上它们为代码审查提供了标准。自动化固然有益,但当规则存在例外条款时——例如“若无法合理实现X,则允许Y替代”——这类情况无法通过静态分析实现标准化。
说得对。MISRA简直是教条主义。实际研究[1][2]表明其多数规则弊大于利。我在多个安全关键行业工作过,执行MISRA的要么是完全不懂源代码的官僚,要么是靠写代码爬升的资深开发者。曾有位经理对Matlab赞不绝口,只因Matlab生成的C代码永远符合MISRA规范,而我们公司提供的代码却屡屡违规。他却忽略了生成的合规代码中每个函数都充斥着tmp01、tmp02、tmp03这类变量。
许多软件领域因官僚要求必须遵守MISRA规范,但这些领域其实并不涉及安全关键性。代码质量堪称一团糟。另有些领域既需MISRA合规,又涉及真正安全关键的领域(如汽车软件)。此处的救赎在于:(1) 每台CPU的代码库复杂度较低,(2) 测试覆盖全面。
对于追求 真正 安全、保密性及可移植性的人,我建议他们借鉴Linux内核、SQLite、OpenSSL、FFmpeg等项目的实践范例。相较于MISRA合规检查器,现代代码检查工具(即便免费版本)才真正具有价值。
[1] https://ieeexplore.ieee.org/abstract/document/4658076
[2] https://repository.tudelft.nl/record/uuid:646de5ba-eee8-4ec8…
该论文被人们忽视的关键点在于,他们采用的是 追溯性 应用编码标准的方法。即对现有代码库运行合规工具,并尝试修复标记出的问题。我认为他们准确指出了这种方法的弊端——在重构现有代码时,存在引入缺陷的风险。至于从项目初期就应用编码规范的情况,我认为他们缺乏充分实证依据。
在我看来,MISRA C++ 2023修订版相较2008版实现了重大飞跃。这是全面的理念革新,提供了更多普适性指导。无论采用何种规范,都需根据项目特性进行定制化调整。就连MISRA规范的制定者也认同:
“”"
“”"
未使用的参数应添加注释。
除非该参数存在是为了符合接口规范
尤其当其存在仅为符合接口规范时。可注释掉变量名而保留类型声明。
令人费解的是,其他评论者竟无人理解此处指出的问题所在。
呃,抱歉出现奇怪的拼写错误。未曾察觉。现已无法编辑。
更何况存在公认的参数忽略方式:
每个脱离新手阶段的C程序员都懂这个。
关键在于:未使用的函数参数几乎都应删除,而非用技巧伪装成已使用——而这种技巧本身就是错误的!
有时确实如此。但当存在通过函数指针调用的函数集时,这些函数需要相同签名,其中一个或多个会忽略某些参数。如今我会用 __attribute__((unused)) 标记,但这完全是合理场景。
上述情况偶尔会出现。参数本身是必需的,但常规构建中并不使用它。
我确信C风格强制转换不允许这样做。
顺便提一句,当启用-Wno-old-style-cast时(禁止C风格转换的项目通常会启用此选项,或使用编译器提供的等效选项),GCC和Clang会忽略“未使用的void转换”情况。
C++17引入了[[maybe_unused]]属性。
某些继承场景难道不是必然的吗?基类实现基础功能时无需所有参数,而派生类需要额外参数。
Laurie的作品太棒了!她频道的其他视频涉及逆向工程、混淆技术、编译器等主题
若喜欢本视频强烈推荐观看她的其他内容
LaurieWired是YouTube上值得关注的优质频道!
她的ARM汇编教程系列实在太出色了
不知洛克希德·马丁是否在研发基于Electron的未来战斗机?
我花了好久才意识到你评论里“Electron”指的是框架。
这套方案在SpaceX和NASA不就用得挺好?
航空电子系统普遍遵循MISRA C/C++规范吗?还是会采用更严格(或不同的)方法?
编码规范只是部分考量。核心在于严格程度,以及为可审计性记录流程与结果。DO-178c标准就是例证。
依我经验取决于公司。见过某些供应商直接用Matlab/Simulink接好电路图就点击自动编码。生成的C代码完全未经人工修改。
坦白说,我认为这可能是编写高可靠性代码的正确方式。
开玩笑吧?自动生成的代码通常是垃圾和意大利面式代码。这很可能就是导致丰田意外加速故障的原因。
丰田/电装事件中,涉事代码同时包含自动生成和人工编写的元素,甚至存在自动生成代码被后期人工修改的情况。这种混合状态最为危险——既丧失了代码生成器提供的结构化保障,又缺乏优秀软件工程师团队手动开发同等复杂度代码时应有的结构化设计与选择余地。
若不将输出视为“源代码”,这未必是问题。汇编语言同样是垃圾般的意大利面代码,但这并未阻止你使用编译器,不是吗?
现代Simulink自动生成的C代码相当高效。它既非垃圾也非意大利面,只是…颇为独特。
它消耗的资源(计算和内存)也远超人类为相同需求编写的代码。
对于航电系统这类控制系统,要么通过认证测试套件,要么直接淘汰。人类能否编写更省内存的代码根本无关紧要。若自动生成的代码性能不足以在设备上运行,只需指定更快的芯片或更大内存即可。
很抱歉,我不同意。构建这类实时安全关键系统正是我的本职工作。系统设计完成且硬件选定后,我认同若所需任务能适配硬件即可投入使用——预留闲置内存并无额外价值。但系统的规模规划,乃至将系统分解为多个电子控制单元(ECU)及集成程度,都取决于代码的效率。这里存在阶跃函数效应——即便十年前,也无法获得性能足以支撑eVTOL控制环路的安全处理器(不可能简单地“选个更快的芯片”),因此系统设计必须在更低ASIL等级的硬件上实现可靠性,代价是更高层次的系统复杂度。如今在安全处理器上实现手写代码已可行,但自动生成代码仍存在局限。这意味着若容忍代码生成的膨胀,系统层面必将付出代价。
>这里存在阶跃函数——即便十年前也无法获得满足eVTOL控制回路性能要求的安全处理器(并非“只需指定更快的芯片”就能解决)
认为十年前的处理器比当今产品更慢并非新颖发现。
这仅意味着十年前必须依赖人工编写代码,而如今可通过自动生成实现更安全的编码。
五十余年间层出不穷的“偏移量偏移”和“内存释放后使用”错误,早该让我们放弃人类能编写安全代码的狂妄幻想。事实证明我们做不到。
在任何其他领域,当人体能力不足时我们都会借助工具。正因如此,我们发明了斧头、螺丝刀和叉车。
但不知为何,在软件领域总有人无视所有反证,执着于“人类能编写安全代码”的荒谬观念。
> 这仅仅说明十年前需要依赖人工编写的代码,如今通过自动生成能更安全地完成。
不,这远不止于此。这里存在交叉效应:一轴是“所需资源”(代码生成消耗更高),另一轴是“可用硬件安全特性”。若代码生成所需的高资源消耗迫使你在该性能区间内减少硬件安全特性,你就只能采用更复杂的安全方案,从而推高整体系统复杂度。选择并非在于“代码生成(伴随工具安全性提升但硬件成本增加)”与“手工编写代码(含需通过测试流程缓解的人为缺陷但硬件成本较低)”之间。而是“代码生成(工具安全性更高、系统复杂度增加、故障注入测试矩阵大幅扩大)”与“手工编写代码(含人为缺陷但系统整体更简洁)”之间的权衡。虽然理论上存在两种极端:系统足够简单时安全处理器可灵活选用,或系统过于复杂时非安全处理器成为必然选择…但据我十年从业经验,现实中存在大量处于临界状态的真实、有趣且具有代表性的系统。
值得补充的是,对于依据DO-178标准达到DAL B或DAL A级别的关键航空电子设备,实际运行中发现的缺陷率极低。这虽需投入惊人时间(资金)进行测试,但确实可实现——现实航空电子系统中的缺陷绝大多数源于需求规格说明书,而非实现环节(无论是否手工编写)。
HN平台难以展开深度讨论,我们只能各持己见。在此格式下我无意继续深入探讨。
Matlab/Simulink等工具的代码生成功能适用于概念验证设计。它主要帮助编程能力有限的工程师探索不同算法方案。而实际将算法部署到系统中的工程师,往往来自具备不同领域专长的团队。
火箭曾依靠自动生成的Simulink代码进入轨道,我亲眼见证过
> 这很可能是导致丰田汽车意外加速故障的原因。
你对“很可能”的论断有证据吗?
我确知Simulink会生成意大利面代码,而意大利面代码正是导致丰田问题的部分原因。故此推论
参见https://www.safetyresearch.net/toyota-unintended-acceleratio…
这完全是牵强附会。“意大利面代码”是个极其宽泛的术语,根本不足以支撑两者存在关联。
“我确知意大利厨师会制作意大利面,而死者遗体内含有意大利面成分,因此必定是意大利厨师下毒”
SRS是一家营利性公司,其收入来源于诉讼案件,因此其报告/调查因财务激励而存在偏颇,往往会夸大调查结果的重要性。
不,我一点也不开玩笑。自动编码功能生成的代码与Simulink模型具有高度一致性,其可靠性远超人工操作。
例如,Simulink模型不可能在输入
i >= 0时误写成i > 0。任何声称从未犯过此类错误的人都是在撒谎。除非丰田存在第二起非指令性加速故障,否则我认为问题根源在于油门踏板的机械设计缺陷导致其卡在脚垫上。
无论如何,当涉及航空电子设备这类安全关键控制系统时,将代码输入编辑器的实际操作抽象化更为稳妥——这能消除潜在错误源。通过更高层级的模型验证,代码便能以确定性方式生成。
> Simulink模型不可能在需要输入
i >= 0时误写成i > 0Simulink Coder工具是软件产品,由人类设计实现,必然存在缺陷。
自动生成的代码与人工编写的代码不同,它会触发C/C++编译器的软弱点。
例如,自动生成的代码可能包含极其庞大的switch语句。你知道吗?其规模远超编译器实现者设定的15位分支偏移量——这个偏移量原本被认为足以处理任何正常人类编写的switch语句。结果现在,当尝试跳转到正确的case语句时,switch反而向后跳转了。
我并非否定Simulink Coder配合C/C++编译器的价值。它或许优于现有的“手动编码”方案,但同样无法保证100%无漏洞。
>但它也并非完全没有漏洞。
没人说过它没有漏洞,这是你自己构造的稻草人论证。
使用自动编码能彻底消除人类C程序员半个多世纪以来持续犯下的特定类型错误。
> 模拟仿真模型不可能在需要写
i >= 0时误写成i > 0这是典型的认知偏见:比较A和B时,只需证明B不存在A的缺陷。若两者本属不同系统,此结论自不待言。但同样成立的是:A也不存在B的缺陷。换言之,自动代码系统存在哪些人类编程不具备的缺陷?
认为机器无懈可击的幻想——本讨论中另一种(隐含)论调——对任何技术从业者而言都只是无知。
自动生成的C代码与编译为汇编或机器代码有何区别?在我看来纯属学术争论。
自动代码的核心缺陷在于人类难以阅读验证,因此无法真正作为源代码使用。根据我的经验,这是此类系统最大的缺陷之一。你必须对生成代码的专有图形化编程软件进行版本控制,尽管我们常抱怨Git,但相比之下它简直堪称奇迹。
> 自动生成的C代码与编译成汇编或机器代码有何区别?在我看来纯属学术争论。
这是个有趣的问题和观点,但两者本质不同,没有理由认为结果会相同。如果该理论成立,为何不直接从自然语言编译?
自然语言没有规范,而C语言和汇编语言都有。
C语言规范复杂度高出几个数量级,且定义远不如汇编明确。反过来说,自然语言与C语言的对比也成立。
我承认这更多是哲学层面的讨论。但“C能自动生成可靠汇编,故规范也能自动生成可靠C代码”这种论断,本质上是在混淆两个不同问题。
取决于地区。MISRA规范被广泛采用,此外还有美国军用标准、欧洲航空航天ECSS标准、航空领域的DO-178C标准…
/?hnlog 安全关键领域
摘自https://news.ycombinator.com/item?id=45562815 :
> awesome-safety-critical: https://awesome-safety-critical.readthedocs.io/en/latest/
摘自《安全C++提案不再推进》(2025) https://news.ycombinator.com/item?id=45237019 :
> 安全C++草案:https://safecpp.org/draft.html
此外还有标准化安全Rust的努力;rust-lang/fls, rustfoundation/safety-critical-rust-consortium
> FLS实现的功能与这些 [遗憾的是已终止] 安全C++提案相比如何?
所谓“90%禁令”并非针对C++的偏见——而是为了确保确定性。在航空电子领域,任何可能隐藏内存分配、引入不可预测控制流或复杂化WCET分析的特性都会被剔除。一旦受限于这些约束,所有语言终将退化为可完全审计的微小子集。
他们完全可以100%使用Rust
F35项目不被视为失败了吗?还是我把它和别的项目搞混了?
无数文章宣称F35已走向末路,但这只是片面之词。早在50年前的1970年代,关于如何打造最佳新一代战斗机的论战就已展开。其中一个阵营被称为“战斗机黑手党”[0],由约翰·博伊德领衔。他们主张战斗机唯一核心价值在于近距格斗性能,声称隐身技术、超视距导弹、电子战及传感器/数据链系统全是无用垃圾,只会削弱格斗能力并推高战机成本。
这一论断的依据源于F-35与老式F-16的空战测试。测试结果显示,除一次特殊情况外F-35几乎全胜——当时一架轻型装备的F-16通过瞬移直接出现在满载重型导弹的F-35后方并赢得战斗。这唯一败绩引发数百篇报道,宣称F-35是无法进行空战的垃圾战机。
归根结底,F-35具备诸多现代作战不可或缺的尖端功能。该机型已在西方国家获得足够订单量,规模经济效应开始显现,单价约8000万美元,这比为其他机体加装隐身与传感器系统(如F-15EX项目)更为经济。
[0] https://en.wikipedia.org/wiki/Fighter_Mafia
确实,再高超的机动也无法替代杀伤链体系——当分布式传感器网络、中继站与武器载具形成协同,就能让空对空导弹以光速从任意方位发动攻击。
许多人靠宣称F-35是失败之作谋生,但尽管该项目并非完美无缺,它确实打造出性能超越绝大多数战机的“廉价”战斗机。
绝非失败之作。
关于它的报道简直垃圾堆成山。说真的,大量文章不过是主流媒体不加批判地转发澳大利亚几个自诩智库的怪人言论。
客观而言,该项目严重超时超支,从这个角度看确实失败。但最终诞生的战机无论在绝对性能还是性价比上,都毫无疑问是同类产品中的佼佼者。
政府内部已形成广泛共识:F-35项目中的管理失误绝不能重蹈覆辙。在无人机技术日新月异的时代,长达三十年的研发周期显然不合时宜。同时政府也意识到,必须以项目主导者的身份加强统筹协调,避免技术锁定问题。
F-35确实经历过漫长的开发困境,但绝非彻底失败。看看最近那些将其作为政治筹码的交易——据我所知,它最终仍是一款极具吸引力且性能卓越的平台。
> 看看它最近作为政治筹码的交易案例;据我所知,它终究仍是备受追捧且性能卓越的平台。
从欧洲视角看,我能明确告诉你:舆论风向已从 “采购美制战机巩固美欧关系” 彻底转向 “战争来临时任何关键需求都不可依赖美国” 。
这与F-35毫无关联。
欧洲完全有智慧和能力自主研发战机平台。
尽管欧洲存在若干可比替代方案,但多国早已押注F-35。它仍是这场讨论的核心议题。
我来自其中一国,可以肯定地说,如今许多人更希望当初选择欧盟的竞争机型。
当前存在什么可比替代方案?欧洲企业既无量产的第五代战机,也缺乏集成化感知能力。这正是尽管存在疑虑却仍引发巨大需求的原因——在近似同级对手的作战环境中,没有这些能力就无法生存。
各国采购美国战机,是因为在某些高价值领域它确实是唯一选择,而非认可单一供应商带来的隐忧。无论好坏,美国已拥有三十年飞行经验,第六代战机正在生产中,而其他国家仍在研发首款第五代战机。
缩小这种差距绝非易事。无论如何,欧洲国家都需要这些现代化能力来构建有效的威慑力量。
我虽非专家,但核心在于具体需求。需注意的是,加入战斗机项目意味着参与研发过程,通过资金投入获得相应话语权。例如,经过充分升级的“鹰狮”战机若能满足我们的需求(据我所知主要任务并非空战),其效能可能相当,且成本更低。
总之我们只能祈祷美国只是暂时失常,终将恢复理智。除此之外还能怎样呢。
> 当前存在哪些可比替代方案?
你心知肚明,但我还是要说:当下没有可比替代方案,近期也不会出现。
你把标题党文章和现实搞混了。
迄今已生产1200余架F-35,年均产量约150架。对比而言,这几乎相当于F-22总产量,而1200架对现代战斗机而言已是庞大数量级。而极为成功的F-15战机自50多年前投产至今,总产量也仅与此相当。
这虽不能证明其必然优秀,但无疑是重要指标。尤其值得注意的是,美国并非唯一客户——众多国家都对其青睐有加。当前部分国家选择回避,纯粹出于政治考量,因美国已不再被视为可靠的供应商。
就实际作战能力而言,除成本高昂且已停产的F-22外,F-35堪称现役最优战机。其价格相对低廉,与“鹰狮”、“阵风”等竞品相当,但性能远超同类机型。
坊间充斥着诸多贬低F-35的文章,主要集中在以下几类:
* 对高昂研发成本、超支延误的合理批评,被无端推演为“战机本身糟糕”
* 将初期故障夸大为“致命缺陷”,仿佛这些问题永无解决之日
* 基于演习结果的分析,却误解了演习的初衷与设计目的 例如你可能看到F-35在模拟空战中败给F-16。但现实中不会安排大量让F-35与F-16进行真实交战的演习——这种演习的结果只会是F-16在不知F-35存在的情况下被击落,既无参考价值又浪费时间金钱。因此此类对抗需设置限制条件才能产生实际意义。这可能演变为空战,届时F-16确实具备优势。结果就会被报道为“F-35逊于F-16”,却忽略了真实战场中F-35早在空战爆发前就已取得胜利的事实。
* 完全合理的论点认为:战斗机已是上世纪的武器,无人机和导弹才是未来,而F-35就像1941年最先进的战列舰——实用且强大,但正迅速过时。这或许属实,但若成立,也仅说明F-35并非值得投入的核心项目,而非其本身是失败品。航空母舰是太平洋战争的制胜武器,但这并不意味着衣阿华级战列舰就是失败之作。
从多方面看,F-35是首款为无人机主导战争需求而专门设计的战机。其局限性在于:这种能力是被嫁接到一套(按美国标准)较旧的第五代技术架构上,该架构从根本设计理念上就不适合承担此类任务。我认为这正是F-22最终产量受限的根源——该机型甚至无法升级至F-35在无人机主导环境中的作战标准。
当前推出的新一代第六代平台(B-21、F-47等)均为纯粹基于第一性原理设计的原生无人机作战平台。
我略带挑衅的回复:若资金无限,失败实属不易。
玩笑归玩笑:若此项目被视为失败,那么全球还有哪个耗资百亿以上的军事项目未曾被贴上失败标签?
依我这个外行之见,全球性价比最高的战斗机当属萨博JAS 39 Gripen。购置与运营成本极低,性能却相当出色。对于没有无限资金漏洞的军队而言,这是绝佳选择。
它在宣称拥有先进防空系统的国家所设防的领空自由翱翔。
该项目的研发成本严重超支,首批量产过程困难重重,平台本身因采用“万能通用”的折中设计(尽管各军种另有专用型号)而存在设计缺陷。
软件开发方面未见特别负面评价,仅在虚拟/增强现实头盔的研发上遭遇困难(据我所知该组件最终未能投入量产)。
供氧系统故障导致飞行员缺氧。
https://www.nwfdailynews.com/story/news/local/2021/08/02/f-3…
电气系统在短路条件下表现不佳。
https://breakingdefense.com/2024/10/marine-corps-reveals-wha…
该项目尚未交付完毕,现因过热问题需对整支舰队进行大修。
https://nationalsecurityjournal.org/the-f-35-fighters-2-big-…
这个项目完全是彻头彻尾的浪费。和平时期根本不该研发这种东西。纯粹是为安抚无所事事的将军和贪婪的国会议员而搞的无谓的登月计划。
F-35战斗机的C标准禁止使用90%的C特性,因为他们真正想要的是带析构函数的C语言。今日思考如何以现代方式编写C语言时,发现GLib竟在纯C中实现了大量实用的C++便利特性。
翻阅JSF编码规范可见:禁止异常处理、禁止标准模板库、禁止多重继承、禁止动态转换,将C精简至裸机状态,仅保留一项关键特性——通过RAII实现的自动析构函数。当变量作用域结束时,清理机制自动触发。这正是他们从C中提炼的核心价值,不禁让我思考:C语言能否在不依赖C编译器及其复杂机制的情况下实现同等效果?
GLib作为实用工具库,通过GCC和Clang扩展的cleanup属性,为C语言提供了更优的字符串处理、数据结构及可移植系统抽象,其内部更蕴藏着自动资源管理的精妙解决方案。该属性允许为变量附加函数标签,当变量作用域结束时自动调用该函数——这本质上实现了C++析构函数的功能,却无需类和虚函数表的开销。
GLib内存管理系统的核心始于两个简单宏:g_autofree和g_autoptr。g_autofree宏看似简单却蕴含玄机。使用该属性声明指针后,当指针作用域结束时,系统会自动调用g_free释放内存。无需手动管理内存,无需在每个返回路径中记忆释放操作,也无需使用goto语句构建清理段。无论正常返回、因错误提前返回,甚至代码意外跳转至异常路径,指针都会被释放。仅此一项便能消除典型C程序中绝大多数内存泄漏,因为绝大多数内存管理仅涉及malloc和free,或在GLib中使用g_malloc和g_free。
g_autoptr宏则更为复杂。g_autofree适用于指向内存的简单指针,而g_autoptr处理需要自定义清理函数的复杂类型。文件句柄需调用fclose,数据库连接需关闭函数,自定义结构体可能需要多步清理。g_autoptr宏接收类型名称后,会自动调用该类型已注册的对应清理函数。这正是GLib展现成熟度的关键——库中已为所有自有类型预先注册清理函数。GError结构体被正确释放,GFile对象解除引用,GInputStream对象关闭并释放。一切运行无碍。
这些宏背后存在名为 G_DEFINE_AUTOPTR_CLEANUP_FUNC 的机制,用于向 GLib 注册自定义类型。开发者需编写能正确销毁结构体的清理函数,随后通过该宏传入类型名与清理函数,此后即可为该类型使用 g_autoptr。该宏会自动生成连接清理属性与函数的粘合代码,并正确处理所有指针间接引用。这至关重要——清理属性传递的是变量指针而非变量本身,对于指针变量则传递双重指针。若处理不当将导致崩溃或内存损坏。
该机制的第三个组成部分是 g_auto,用于处理栈分配类型。某些 GLib 类型(如 GString)虽设计为驻留栈上,仍需清理机制支持。GString 内部会为缓冲区分配内存,尽管其结构体本身位于栈上。g_auto 宏确保当结构体作用域结束时,其清理函数会运行以释放内部分配的资源。堆指针、复合对象和栈结构均能获得自动清理。
该系统的精妙之处在于其组合方式。函数可同时执行文件打开、缓冲区分配、错误对象创建及复杂数据结构构建等操作,只需为每个资源声明对应的auto宏即可。若任何操作失败导致提前返回,此前声明的所有资源将按声明顺序的逆序自动清理。这与C++析构函数按构造顺序逆向运行的机制完全一致,但您编写的纯C代码可兼容过去十五年间任何GCC或Clang编译器。
这一切的基础是GLib的内存分配函数。该库提供的g_malloc、g_new、g_realloc等函数可直接替代标准C分配函数。这些函数具备更优的错误处理机制——g_malloc绝不返回NULL。若分配失败,程序将终止并输出清晰的错误信息。这听起来或许极端,但对多数应用而言实为正确行为。在传统C代码中,当malloc返回NULL时,多数程序员要么不检查返回值,要么检查方式错误,要么虽检查但缺乏合理的恢复路径。GLib正视这一现实,明确规定契约:若内存分配失败,程序应干净利落地终止,而非继续运行导致未定义行为。
在UI元素和C语言向后兼容性方面,我非常推崇GLib/旧版ObjC的方案。但就我们讨论的嵌入式系统而言,无论通过malloc还是面向对象方式动态创建和销毁对象,我都难以想象其适用场景——即便在HUD界面中,若我需要士兵们安全撤离,仍会优先考虑其他方案。
若需处理内存分配失败,GLib提供了可返回NULL的g_try_malloc及相关函数。核心思路是让常规情况自动处理,异常情况显式处理。其中g_new宏尤为出色,因其具备类型感知能力。无需编写多次sizeof乘以count的g_malloc再进行强制类型转换,只需写g_new类型和count,它会自动处理尺寸计算和类型转换,同时检查乘法运算是否溢出。
引用计数是GLib内存管理(特别是对象管理)的另一关键组件。GObject系统(GLib为C语言提供的对象系统)采用引用计数机制管理对象生命周期。每个对象创建时初始引用计数为1。当需要保留对象引用时调用g_object_ref,使用完毕后调用g_object_unref。当引用计数归零时,对象将自动销毁。该机制与C++的shared_ptr或Python的引用计数原理相同,但采用纯C语言实现。
该机制还与autoptr系统集成。GLib中许多类型都采用引用计数,其清理函数仅需递减引用计数值。这意味着你可以用g_autoptr声明局部变量:使用期间引用计数保持正值,变量作用域结束时引用自动释放。若您是该引用的最后持有者,对象即被释放;若代码其他部分仍持有引用,对象则保持存活。这解决了C语言中手动内存管理困难的资源共享问题。
GLib还通过GMemChunk及更新的切片分配器提供内存池功能,不过随着现代分配器性能显著提升,切片分配器正逐步被标准malloc取代。该机制旨在为频繁分配相同尺寸小对象的程序降低分配开销和碎片化问题。开发者可创建特定尺寸对象的内存池,直接从池中快速分配,无需调用通用分配器。当池中所有对象使用完毕后,可一次性销毁整个内存池。此模式常见于高性能C程序,而GLib将其作为可复用组件提供。
GLib中的错误处理机制值得特别关注,因为它展示了自动清理如何实现更优的错误处理模式。GError类型是一种结构体,承载着包含域、代码和消息的错误信息。可能失败的函数将GError双指针作为最后一个参数。若函数成功,则返回真值或有效值,并将错误指针设为NULL;若失败,则返回假值或NULL,并分配一个包含错误详情的GError对象。调用代码检查返回值,若存在错误则解析GError获取详细信息。
关键点在于:使用 g_autoptr 声明的 GError 会自动释放。你可以编写调用十项不同操作的函数,每项操作都可能引发错误。即使在所有代码路径中都检查每个操作并提前返回,错误也会自动释放。您永远不必担心错误消息字符串泄漏、双重释放或遗忘释放的问题。相较于传统C语言错误处理——要么忽略错误,要么编写包含跳转到函数末尾标签的goto语句的繁琐清理代码——这堪称巨大进步。
GNOME开发者本可转向C++、Rust等现代语言,但他们选择在C语言的优势领域进行优化。通过添加恰到好处的基础设施消除常见陷阱,却未改变语言本质。C程序员阅读GLib代码时能立即理解,因为它本质仍是C语言。auto宏只是编译器属性的语法糖,而非需要定制编译器的新语言特性。
这种理念与F-35开发者的需求高度契合:既要C语言的性能与可预测性,又要自动资源管理的安全性。没有隐式分配、没有虚拟调度开销、没有异常展开成本、没有模板实例化膨胀。仅有确定性的清理机制——因其与词法作用域绑定,你完全能通过代码直观验证清理时机。
令人意外的是,现代C语言的解决方案并非新语言或颠覆传统实践。清理属性早在2003年就已存在于GCC中,引用计数机制更是历史悠久。真正的创新在于将这些组件整合为一个逻辑自洽的系统——它既符合直觉又具备良好的组合性。
有时最佳工具并非最新潮的,而是能以最小额外复杂度解决实际问题的方案。GLib证明:借助稳定数十年的编译器,C语言今日即可实现该特性,且无需牺牲使其成为宝贵语言的简洁性与可预测性。
你忽略了关键背景:GNOME的诞生源于与KDE的许可分歧,而原始FSF因宗教原因反对C++。即便KDE/QT采用GPL兼容许可,他们也不会采纳。
若放眼Linux之外的世界,所有人都在拥抱C++——PC领域的OS/2、MS-DOS与Windows,苹果、Epoch(后更名Symbian)、BeOS…… UNIX阵营则在探索CORBA、OpenInventor等技术。
以下是《GNU宣言》原始版本:
“使用C语言以外的编程语言如同启用非标准特性:这会给用户带来麻烦。即便GCC支持其他语言,用户仍可能因需额外安装编译器来构建你的程序而感到不便。因此请务必使用C语言编写。”
1994年《GNU编码规范》http://web.mit.edu/gnu/doc/html/standards_7.html#SEC12
时间推进至1998年,当时GNOME 1.0尚在筹备阶段:
"使用C语言以外的编程语言如同启用非标准特性:这会给用户带来麻烦。即便GCC支持其他语言,用户仍可能因需安装该语言编译器才能构建程序而感到不便。例如,若你用C编写程序,人们就必须安装C编译器才能编译你的程序。因此,用C语言编写更为妥当。"
https://www.ime.usp.br/~jose/standards.html#SEC9
是的,实际版本对编程语言多样性更为包容,
https://www.gnu.org/prep/standards/html_node/Source-Language…
不知这些标准与高频交易训练标准相比如何?关键路径中似乎存在相似的速度/可靠性/可预测性要求。
JFS-CPP禁止异常处理,因为这会导致对问题执行的控制权丧失。高频交易群体不喜欢它,因为函数调用会因此增加10纳秒延迟。
至少以前我们还有零成本跳过分支的情况。如今,我怀疑高频交易者们又开始精算微秒或毫秒了——因为交易变得更聪明了,而非更快。
至少有些高频交易者确实利用异常处理来规避低频但速度关键的执行路径中的分支:https://youtu.be/KHlI5NBbIPY?si=VjFs7xVN0GsectHr
没想到对类型要求不那么严格。记得谷歌不允许无符号类型?
更值得探讨的是:
哪种方式能提升代码可读性并减少错误
异常处理(几乎所有语言都采用)还是错误代码 (如Go语言)
这里有人选择完全放弃异常而使用错误代码吗?
这没什么好讨论的。对于底层系统代码,异常会引入大量问题和棘手的边界情况。错误代码在此场景下更简洁、更高效且更易于推理。几乎所有系统语言都采用错误代码。
在同时支持两种机制的C中,系统代码通常会在编译时禁用异常。这已成惯例,我从未接触过使用异常的C代码库。反之,高级非系统C++代码可能采用异常机制。
你的描述在历史上是正确的,但新分析表明:若实际执行错误码检查,异常处理反而比错误码更快。当然错误码检查很繁琐,因此人们往往省略这一步。此外微基准测试显示错误码更高效,只有在更复杂的基准测试中异常处理才会显现出速度优势。
感谢说明。
你是说战斗机居然是用C++编写的?天啊
已故的罗伯特·杜瓦曾戏称现代战斗机并非安全关键型应用——因为计算机系统一旦失效,整架飞机就会当场解体。
“发射核火箭”这句话现在字面意义上成立了。
像战斗机飞行员那样编程?这根本说不通。
AUTOSAR免费PDF文件《关键与安全相关系统中C14语言使用指南》(定义为MISRA C 2008的更新版)- http://www.autosar.org/fileadmin/standards/R18-10_R4.4.0_R1….
请注意,MISRA与AUTOSAR的指南现已整合为单一标准“MISRA C++ 2023”,该标准已针对C++17进行更新。
解析AUTOSAR C++14编码规范 – https://www.parasoft.com/blog/breaking-down-the-autosar-c14-…
她关于异常与错误代码的观点是:若未能捕获特定异常,系统便会陷入混乱;而若改为“捕获”错误代码,一切便能井然有序。但事实上,错误代码同样可能被忽视。
当然这并非意味着异常与错误代码本质相同。
1994年的C编译器漏洞百出,而C功能清单的现代化改造至今仍困在某个委员会里?
呼叫Ada粉丝团!你们错过了重点!
若1994年乔·阿姆斯特朗和艾伦·凯要列出7种替代C++的战斗机编程语言,他们会怎么做?
简而言之:
– 禁止异常
– 禁止递归
– 内循环禁止malloc()/free()
我曾参与广播电视的节目播出系统开发。该软件需持续运行数年且零内存泄漏,必须确保每次都精准按时输出电视画面帧。
虽然使用“C++”,但我们遵循相同标准:静态内存分配、无异常处理、无递归。不使用模板,极少用到继承。更像是带类的C语言。
我从事相同领域多年;要求如出一辙——广播播出系统,数年不间断运行,绝不遗漏任何帧。
当时的C++代码堪称灾难。自制的引用计数机制存在线程风险,而多层嵌套继承的对象类型不同时,计数器时而递增时而停滞。整个对象由怪异的继承链构成。命名体系也荒谬至极:“pencilFactory”并非制造铅笔的工厂,而是指工厂生产的任何铅笔制品。继承而非组合显然是核心设计模式——若需其他对象的功能,就直接继承该对象。这导致某些对象竟从同一个类继承了六次之多。
多重继承系统在对象创建时被赋予了诡异的控制权:通过特殊函数定义它们可转换为哪些对象类型(从实际拥有的所有类型集合中选择),但每当有人需要转换为列表外的类型时,他们照样会直接用C进行强制转换。你必须强制转换,因为所有函数都被刻意设为私有——就是为了逼你转换。但这种转换方式绝非C所期望的——绝非如此!
那些疯狂的自制容器如同Win32不透明对象:你只能获取目标对象的空指针,获取下一个对象时再将该空指针传回。显然这是在模仿MS COM的IUnknown接口,以及其他诸如QueryInterface之类的自创方案,实质上是在C++之上构建了专属的继承体系。
我真正领悟的是:即便采用最荒谬绝伦的架构决策——那些昭示着原始设计者始终用C思维思考,却用C构建其糟糕C实现的决策——依然能打造出持续运行数年且保持帧级精度的系统。而他确实是用C++写就了这一切。
天哪,这段回忆之旅真是妙不可言。
多重继承系统绝非“用C思维思考”者能构想的产物。这更像是典型的C++混乱代码。
职业生涯早期我参与过纯C系统开发。他们用纯C实现了多重继承(类似Perl/Python的MRO机制)。虽然疯狂,但因未滥用反而运行良好。
严肃提问:是否存在不使用多重继承的GUI工具包?连Java Swing都通过接口实现了多重继承(.NET应该也有类似方案)。Qt更是到处都是多重继承。
收回前言 😉 人们总能想出疯狂的点子。不过我仍不认为这属于“C思维”范畴。用C语言构建面向对象代码其实很常见,效果也相当不错。
我能想到的最佳范例是Win32控件界面(user32/创建窗口/注册类)的C语言实现。虽然你可能无法阅读其源代码,但可参考Wine的实现方式或其替代方案(如NetBSD的PEACE运行时,现已弃用)。
实际上我所知唯一模仿这种风格的工具包是Nakst的Luigi工具包(同样用C实现)。
两者都未真正使用继承,而是通过向不同控件发送“消息传递”来实现组合。
有人会说C工具包使用多重继承是因为C没有接口。
据我所知,GTK并不支持多重继承。
它确实不支持,但绝对“实现了”单继承树(通过向上/向下转换),我认为Xt工具包(如Motif)也采用了这种方式。
耐人寻味的是,那些本应采用先进软件开发实践的领域反而恰恰相反——而备受诟病的“科技巨头”里的“代码猴子”们,实际水平往往相当出色。
最令我痛心的是,我遇见过最顶尖的程序员竟活跃在高频交易、金融领域,以及那些批量生产垃圾代码的广告技术公司。
我至今仍渴望投身嵌入式领域,在8KB内存限制下编写零浪费的完美代码。但从他人经验得知,嵌入式软件往往聚集着最差劲的开发者和最糟糕的开发实践。
是否有人也在非关键场景(如Web应用)中基本禁用异常处理?
我认为这是正确方向,因为它不会模糊控制流。我也考虑过像TigerBeetle那样添加断言机制
https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TI…
多数大型开源项目禁止使用异常,通常是因为项目最初由C语言转换而来,与非局部控制流不兼容。或是项目源自某个组织,该组织拥有大量不支持异常处理的C++代码,且需要与之集成。
不过某些大型商业软件系统仍采用C++异常机制。
直至近年,几乎所有实现都在抛出路径设置全局互斥锁。随着核心数量激增,进程中可承受的抛出频率竟变得异常缓慢。但GCC/libstdc在glibc中已移除该锁。期待其他实现也能跟进,避免C再添一套冗余的错误处理方案。
谷歌风格禁止使用它们:https://google.github.io/styleguide/cppguide.html#Exceptions
许多游戏引擎(尤其是虚幻引擎)在编译时都不支持异常处理。当年开发EASTL部分原因正是为了规避Dinkumware STL和STLport对异常处理的糟糕支持。
据我所知,所有知名引擎团队都禁止使用异常处理。它们不仅毫无用处,反而弊大于利
没错,实时代码同样如此。new/malloc/free/delete会使用隐藏互斥锁,可能导致优先级倒置——海森堡级别的错误,比如偶发性音视频卡顿却难以定位。编码时最好规避这些操作
它们还可能直接失败——比如内存耗尽或堆内存严重碎片化时。更糟的是执行时间不可预测,若试图证明满足最坏情况的时序要求,这将造成严重问题。
这在游戏行业也是标准做法。此外还有诸多限制:禁用运行时类型信息(RTTI)、避免依赖Boost等庞大库、禁用智能指针、尽量规避构造函数/析构函数等。
这根本算不上C++的90%。
若启用-fno-exceptions编译选项,STL几乎全线失效。
其实可以启用异常编译STL,但严格禁止初始化后的动态分配。具体取决于你追求的规范严格程度。
我的经验不同。我维护的代码库禁用了异常处理,STL功能仍相当完备。(异常处理带来的二进制体积开销出乎意料地巨大。)
据Khalil Estel在ACCU和CPPCon的演讲,即使在嵌入式系统中也能大幅缓解此问题,使大小开销降低数个数量级。
需要查证。你指的是这些内容吧:
– C++异常可缩减固件代码体积,ACCU[1]
– C++异常实现更小固件,CppCon[2]
[1]: https://www.youtube.com/watch?v=BGmzMuSDt-Y
[2]: https://www.youtube.com/watch?v=bY2FlayomlE
是的。可惜我后来转到一个APU团队,代码体积不再是问题,因此没机会验证这种分析方法在我实际工作中能否奏效。
不过这确实是场发人深省的讨论,至少从体积角度而言,它颠覆了深度嵌入式编程的基石之一。
虽不完全了解你的具体情况,但若你在禁用异常处理的代码库中工作,就会知道STL容器在此环境下基本无法使用(似乎只有std::tuple例外,详见下方独立评论)。我认为STL的大多数应用场景都依赖于其容器特性。
那么你的代码库具体使用了STL哪些部分?想必主要是编译时功能(类型、类型特征等)。
在禁用异常的环境中仍可使用std容器。但需注意:一旦发生错误程序将直接终止。
我们禁止异常!若发生异常,直接忽略不理。
那你们就根本不能用这些特性。
处理C++容器的边界条件时,依赖异常处理并不常见。
我的意思是,.at确实很棒,但它真正的作用在于消除未定义行为——而程序直接终止恰恰实现了这个目标。不过我也见过解码器直接捕获std::out_of_range甚至std::exception来处理逻辑中的残余错误。
你了解STL的独立运行定义吗?详见此处:https://en.cppreference.com/w/cpp/freestanding.html 若使用新版C++标准,其中大量实用功能均可启用。
主要涉及类型定义和编译器相关功能,比如type_traits。不过令人惊喜的是std::tuple已完全支持。看来C++26将大幅增强独立模式的支持。
但算法和容器未被纳入,而这些在我看来占STL核心功能的90%。
标准库中malloc/free的大部分功能。
但若所有内存都在启动时静态初始化,这些功能缺失影响不大。
或者抛出异常。
这正是我经手过所有C++代码库的标准做法
你在哪行当干活?现代RAII实践相当普遍啊
嵌入式系统中很常见,毕竟内存有限又没有操作系统来执行垃圾回收。
C++里有垃圾回收?
RAII和上述内容有何关联?
程序初始化后零分配。
RAII本身并不涉及内存分配。
我猜你可能默认所有用户定义类型(甚至所有非平凡内置类型)都经过装箱处理——即创建时会被分配在堆上。
在C++(本文讨论的语言)中并非如此,其他现代语言中也极少采用这种做法,因为它存在严重的性能缺陷。
在构造函数中打开文件,在析构函数中关闭文件。实现零分配的RAII机制。
在栈上分配和释放的
std::vector<int>会为其整数在堆上分配数组…听说MSVC会(曾经?)这样做,但若属实那属于MSVC的问题。gcc和clang不会这样。
https://godbolt.org/z/nasoWeq5M
你的意思是?Vector 本质上是对动态大小数组的抽象,当然会使用堆存储元素。
RAII 难道不一定需要内存分配?
栈内“分配”基本无需成本。
不。而且它们不安全。务必避免使用。
若使用标准库,你本就不必关注分配与释放问题。比如使用std::string。所以我想问的是,你所在的行业是否刻意回避标准库?
我在大规模数据基础设施领域工作。启动后不进行内存分配是常见做法。尽管如此,标准库的大部分功能依然可用,不过不使用标准容器还有其他原因。例如,我们常需要跨进程边界进行存储分页的容器。
C++的设计让这变得相当容易。
虽非专家,但我确信禁用异常意味着无法使用std算法库或容器库的多数功能。
若采用内存池机制,RAII的实现难度也会大幅增加。
https://en.cppreference.com/w/cpp/freestanding.html 可查看可用组件列表。
“现代”一词与此事有何关联?
禁止递归实在令人困扰。Rust未来规划中值得期待的特性之一,是引入显式尾递归运算符(暂命名为
become)。正如这段视频(我未点击链接但推测是Laurie近期视频)所解释的,与存在栈溢出风险的简单递归不同,优化后的尾递归不会增长栈空间。become的核心思想是向编译器发出“此处可实现尾递归”的信号。编译器要么认同该判断并生成优化后的机器码,要么判定不可行导致程序编译失败——无论哪种情况都不会引发栈溢出。Rust的Drop机制在此引入了变数:理论上若每个函数foo创建Goose对象后,多数情况下会再次调用foo,我们不应等到函数返回时才释放每个Goose(此时已为时过晚),这反而会使调用本身成为尾递归。据我理解,
become特性将识别此类情况,提前释放Goose(或拒绝编译)以支持优化。递归思维是一回事,但实在想不起上次在实际代码中需要用到递归是什么时候了。
在C语言中,尾递归的重写相当简单,我实在想不出会有什么复杂情况。
但…这种重写会增加代码的圈复杂度,而他们对此有严格限制,或许这就是禁止递归的原因?当然还有栈溢出的问题。
我不认为仅是环形复杂度的问题。至少部分原因在于需要证明满足硬性实时约束。相比“
for (i = 0; i < 16; i++) ...”这样的循环,递归更难进行此类分析。尾递归运算符是个好主意,但额外的
become关键字很烦人。我认为语法应采用return as:既利用现有关键字,又避免歧义,且以return开头——尾递归本就是其特例。传统上,关于具体语法的争论往往发生在稳定化阶段临近之时。
由于Rust允许(在当前时间跨度内)通过版本更新保留新关键字,因此增设新词并非难题。我通常更倾向于创造新词而非复用旧词,但很乐意了解正反双方的论据。
反对装饰性
return关键词的常见论点是:真正的尾调用并非真正的“返回”,因为它必须先丢弃未传递至尾调用的局部变量。我认为这个论点并不充分——若隐式丢弃的具体位置如此关键,我们本应要求显式丢弃。这基本就是2010年前的电子游戏
将动态内存限制放宽为“每事件内存分配受限于递增分配器”后,该规则至今仍适用于我参与开发的多数AAA/AAAA级游戏。
开发者们都变得懒惰了。听到你们至少还在努力,我感到欣慰。
不,我也懒。
但_你_是_被选中的人.jpeg
既然如此,干脆用C语言写不就得了?他们以为这是C/C++,搞不清两者的区别吗?
> 禁止递归
这到底是完全禁止递归,还是仅限制栈使用?比如处理树结构时,即使不用栈而用数组记录进度,本质上仍是递归操作。真正的关键在于控制内存消耗,这需要限制输入规模。
半认真的提议:许多人(包括我)写的其实是C++,本质上就是C加上少量真正符合人体工学且实用的C++特性(如引用)。这应该标准化为一种新语言,命名为C+
这或许比他们创造的怪兽更成功。我离开C++领域已久,如今几乎认不出这门语言了。
长期以来,至少在微软和英特尔,C++编译器都优于C编译器。
你可能仍会需要使用类(在合理场景下)、引用(比指针更简洁的语法)、运算符重载等特性。例如线性代数库在C++中编写和使用都更优雅。
函数重载也很棒
关于递归:她在视频中解释过。按要求,栈容量必须可静态验证,且不依赖运行时输入。
[已删除]
他们的多数论点并不适用于我的问题场景。
可能恰恰相反!只是在你的场景中,失败代价足够低才得以蒙混过关。但多数资深程序员倾向于固守简单可靠的严格子集,尽可能远离那些架构师幻想的特性。
即便禁用90%的C++特性,该语言仍比其他编程语言庞大约5倍
看看C# 10、Python 3.14、D语言、Haskell…
我不认为C#是庞大的语言。新增特性大多消除了冗余代码。相比之下,年轻得多的Swift在我看来复杂得多
维护C#项目的开发者可能遇到追溯至C# 1.0的遗留代码。
此外自C# 7起持续改进的底层编程特性,以及若干语义变更,并非为消除冗余代码而设计。
更何况语言若无标准库便毫无用武之地,因此P/Invoke调用、COM互操作、Web应用开发等领域都经历了大量变更,自然还需掌握各版本特性引入的时间节点。
啊哈。原来他们用的是C++……
这就能解释F-35项目为何屡屡延期……
你觉得战斗机应该改用Ruby on Rails吗?
任何战机都不该在轨道上运行。
那航母上的弹射轨道呢?
你觉得哪种语言显然更优?
这确实是个好问题。认真说:真好奇他们认真考虑过哪些语言?比如:分析方案里肯定包含C语言。另外,他们是否考虑过编译器扩展?例如:既然C语言没有析构函数,或许可以添加编译器扩展引入defer关键字,让开发者能安排对象销毁时机。即便最终选定C++,我敢肯定在确定允许哪些特性时也引发过小规模的圣战。要知道1990年代启动JSF项目时,C++编译器质量相当糟糕!
当时Ada和C++是唯一现实的选择,而Ada开发者难以招聘。
但坦白说,此类编程中语言差异并不重要。正如指南所示,你将自身限制在语言子集范围内,此时语言差异已无实质意义。基本上所有操作都基于静态分配的全局变量和数组运行。若完全不涉及内存分配,自然无需担心碎片化和垃圾回收问题。关键在于尽可能消除执行过程中的所有变量来源。
因此只要能掌控内存布局,任何C类语言都能实现这种编程方式。
据我所知,传统上航空电子设备采用Ada语言开发,但据网络说法,大型项目难以招募足够的Ada程序员,因此转而采用C++。
Rust
他们应该用Rust
尚未完全实现:https://ferrocene.dev/en?ref=blog.pictor.us
是时候用Rust重写了。
一场关于杀害数百万无辜者的系统性讨论,气氛却异常轻松愉快。下次视频要不要也用同样手法聊聊纳粹毒气室之类的话题?
和毒气室扯上关系真奇怪…你觉得把发达国家军队降级到第三世界水平,就能减少“种族灭绝”吗?
技术本身并非邪恶,邪恶的是操纵技术的人。这种类比未免有些虚伪,恕我直言,甚至有些缺乏敏感度。