愤世嫉俗者

  下载本文的 PDF 版本 PDF

Franglais 编程

Rodney Bates,威奇托州立大学

当我在高中学习法语时,我们学生经常说“Franglais”:我们知道法语语法和单词时就用法语,在我们法语不足的地方就插入英语。这很糟糕,老师也不太赞赏。但是我们可以断断续续地交流,因为我们对各自语言的掌握程度大致相同。

今天,存在一种程序员的 Franglais,它非常普遍。那些年纪足够大的人会记得 20 世纪 60 年代末和 70 年代初关于编译器、操作系统和其他系统程序应该用汇编代码还是高级语言编写的激烈争论。支持高级语言的主要论点是,它们更高级别的计算模型允许在相同的开发时间内编写更多的功能。

在这个问题上,我是一个缓慢的接受者,直到出现了类型系统足够丰富以支持复杂的链接数据结构和内存管理的语言,这促使我跨过了这条线。不幸的是,三十年后,我们仍然没有真正达到目标。问题在于,在最流行的语言 C 和 C++ 中,存在大量未定义和无法定义的行为,因为实际上,编程必须在两种截然不同的计算模型的混淆中完成。

未定义,但仍在语言范围内

首先,让我们看一下一个较温和的未定义行为的例子,尽管如此,它并没有迫使我们超出高级语言模型的范围。大多数语言(Java 是一个值得注意的例外)都没有定义调用实际参数的评估顺序,而是将其留给实现者。通常,二元运算符操作数的评估顺序也是如此。标准的理由是,当代码生成器即将耗尽寄存器时,如果它可以重新排序评估,则可能能够避免存储和重新加载一个寄存器。

这可能会使程序的行为有所不同。例如,假设我写了

f(g(),y);

如果 g 的执行改变了 y,这意味着编译器选择先评估 g() 还是后评估 y 将改变 f 收到的参数,并且可能也会改变 f 的结果。程序员自行承担责任,不要编写依赖此顺序的代码。否则,他们将不会收到任何错误消息,无论是在编译时还是运行时。

程序有可能按预期工作——或看起来按预期工作——但突然在代码的其他地方发生看似无关的更改后崩溃,这会改变编译器对寄存器的使用。像这样的错误不会经常发生,但是一旦发生,通常需要花费大量的时间来发现、诊断和修复,这对项目的影响与它看似的简单性不成比例。

这不是理想的编程语言设计,但至少有两个缓解情况。程序员可以使用高级语言的概念来枚举和理解可能发生的所有行为。评估顺序的排列组合数量当然会随着参数数量的增加而呈阶乘增长,但是如果您花时间,则可以以合理的方式理解每一种排列组合。

语言设计者完全可以轻松地通过仅定义评估顺序(可能是从左到右)来消除这个问题。这只会给语言定义增加两三句话。编译器编写者也很容易遵守——事实上,比利用未定义的评估顺序更容易。

一个不可行的替代方案是保持评估顺序未定义,但期望编译器检测到其行为依赖于实际选择的顺序的代码。这是一个经典的不可判定问题,因此总会有无限多的编码情况,编译器无法确定。它能做的最好的事情是,只要它不能确定没有问题,就生成一个错误。

未定义,并强制进入 Franglais

但是,还有一种更严重的未定义性。C 和 C++ 中有很大一部分区域不仅是未定义的,而且根本无法定义。定义和理解它们都需要转换为汇编程序员的数据模型。在那里,数据是一个大的线性存储单元数组,所有存储单元都具有相同的大小和可能的取值范围。语言中的每种类型都有一种不同的方式将其值编码为机器级别的位和存储单元。其中一些编码可以由语言定义(尽管它们很少被定义),但某些方面不可避免地依赖于机器。

当您开始进行超出对象边缘的内存访问时,语言模型和机器级模型之间的差距会变得更大。在每种高级语言中,数据都分为我将称之为“完整对象”的内容。通过这一点,我的意思是那些不是结构体或类的字段、数组元素或某些包含对象的其他子组件的对象。我包括局部变量和全局变量,以及堆分配的对象。每个完整对象都独立于所有其他对象。访问超出其边界的任何数据都是毫无意义的。

然而,不安全的语言允许这种情况很容易发生——例如,通过将指向一种类型对象的指针转换为指向不同大小对象的指针,或者,当然,转换为数组越界错误。要定义这一点,您必须求助于汇编程序员的数据模型,即将数据视为占据一个大的、线性的、同质的固定大小单元数组。高级语言模型中的对象在机器模型中彼此相邻地布置在内存中。机器代码和匿名常量与程序员没有明确编码的各种东西混合在一起——例如,堆栈链接、寄存器溢出区域等。链接库在上述所有类别中贡献了自己的东西。

为了理解当您超出对象的边界时会发生什么,您必须知道所有这些东西的放置位置。它是通过一个复杂的阴谋发生的,该阴谋涉及编译器、链接器、加载器、堆管理器、所有源代码(包括链接库),以及可能还有其他一些东西,例如操作系统的内存管理和保护方案。所有这些都不可避免地高度依赖于平台的硬件和软件。它不仅在 C 和 C++ 中未定义,而且也无法使用该语言的数据模型来定义。

这就是编程的 Franglais。它是高级和汇编级概念的大杂烩。它使我在开头提到的说法成为谎言,即用高级语言编程可以让您摆脱机器级计算模型。

我们走了多远?

C 在某些领域取得了进展。如果您忽略指针算术、数组下标以及相关的东西(例如字符串的表示方式和堆分配的完成方式),那么 C 中剩余的大部分操作都可以用其自身的类型系统来理解。它们是更高级别的,因为程序员不需要考虑寄存器或寻址模式。表达式运算符在很大程度上是独立于机器的,尽管表面上故意将模运算符(“%”)降级为认为速度比可预测的答案更重要。

C 和 C++ 标准试图通过我所说的“肯定未定义性”来处理无法在该语言自身模型中定义的区域。由于定义它们是无望的,因此参考手册反而详细列举了程序员可以编写的所有未定义的内容。“未定义”是一个技术术语,本质上意味着如果您这样做,您既不会获得您可以依赖的行为,也不会获得错误消息,无论是在编译时还是运行时。

对于一个凡人程序员来说,未定义的区域是一个庞大且划分不公的领土,他必须仅仅通过纪律来记住远离它们。

我们可以做得更好。从与 C 大致相同的时间开始,各种各样的语言提供了大部分类型安全的工具,这些工具只需要极少地被规避,并使这些工具在单独的编译中也能工作。Pascal、Modula、Ada、Oberon 和 Java 的几种变体都是例子。它们在很大程度上实现了高级语言允许程序员摆脱机器语言数据模型的承诺。但不幸的是,用最流行的语言编程就像不得不与一个既不精通英语也不精通法语,而只精通 Franglais 的人交流。程序员必须能够同时以两种截然不同的模型进行思考,才能理解他们正在处理的代码。

RODNEY BATES ([email protected]) 从未从事过他所受训的电气工程方面的有偿工作。相反,他已经在计算机软件领域工作了 33 年,三分之二在工业界,其余在学术界。他主要参与操作系统、编译器以及作为常驻编程语言律师。他是威奇托州立大学计算机科学系的助理教授和研究生协调员。

acmqueue

最初发表于 Queue 第 2 卷,第 8 期
数字图书馆 中评论本文





更多相关文章

Catherine Hayes, David Malone - 质疑评估非加密哈希函数的标准
尽管加密和非加密哈希函数无处不在,但在它们的设计方式上似乎存在差距。密码哈希存在许多由各种安全需求驱动的标准,但在非密码方面,存在一定的传统,尽管哈希函数历史悠久,但尚未得到充分探索。虽然针对现实世界数据集的均匀分布很有意义,但当面对具有特定模式的数据集时,这可能是一个挑战。


Nicole Forsgren, Eirini Kalliamvakou, Abi Noda, Michaela Greiler, Brian Houck, Margaret-Anne Storey - DevEx 在行动
随着领导者寻求在财政紧缩和人工智能等变革性技术的背景下优化软件交付,DevEx(开发者体验)在许多软件组织中越来越受到关注。技术领导者凭直觉接受良好的开发者体验能够实现更有效的软件交付和开发者幸福感。然而,在许多组织中,旨在改进 DevEx 的拟议倡议和投资难以获得支持,因为业务利益相关者质疑改进的价值主张。


João Varajão, António Trigo, Miguel Almeida - 低代码开发生产力
本文旨在通过介绍使用基于代码、低代码和极限低代码技术进行的实验室实验结果,以研究生产力差异,从而为该主题提供新的见解。低代码技术已清晰地显示出更高的生产力水平,为低代码在短期/中期内主导软件开发主流提供了强有力的论据。本文报告了程序和协议、结果、局限性以及未来研究的机会。


Ivar Jacobson, Alistair Cockburn - 用例至关重要
虽然软件行业是一个快节奏且令人兴奋的世界,其中不断开发新的工具、技术和技巧来为商业和社会服务,但它也很健忘。在其快速前进的匆忙中,它容易受到时尚的摆布,并且可能会忘记或忽略已证实的解决其面临的一些永恒问题的方案。用例于 1986 年首次引入,后来广受欢迎,就是这些已证实的解决方案之一。





© 保留所有权利。

© . All rights reserved.