C 与 C++ 40 年的爱恨情仇

来自:CSDN 2020-09-23

作者 | cor3ntin

译者 | 弯月,责编 | 郑丽媛

头图 | CSDN 下载自视觉中国

出品 | CSDN(ID:CSDNnews)

以下为译文:

70 年代初,贝尔实验室创建了 C 语言,它是开发 UNIX 的副产品。很快 C 就成为了最受欢迎的编程语言之一。但是对于 Bjarne Stroustrup 来说,C 的表达能力还不够。于是,他在 1983 年的博士论文中扩展了 C 语言。

于是,支持类的 C 语言诞生了。

当时,Bjarne Stroustrup 明白编程语言有许多组成部分,除了语言本身,还有编译器、链接器和各种库。提供熟悉的工具有助于语言被广泛接受。在这种历史背景下,在 C 语言的基础上开发 C++ 也是有道理的。

40 年后,C 和 C++ 都在行业中得到了广泛使用。但是,互联网上的 C 开发人员认为 C++ 是有史以来最糟糕的人类发明,而许多 C++ 开发人员则希望有朝一日 C 语言灰飞烟灭。

究竟发生了什么事?

从表面上看,C 和 C++ 都可以满足相同的用例:高性能、确定性、原生但可移植的代码,可用于最广泛的硬件和应用程序。

但是,更让 C 自豪的是它是一门低级语言,更接近汇编。

而 C++,从诞生第一天开始就充斥了各种奇怪的东西。例如析构函数这个黑魔法。自作主张的编译器。尽管很早 C++ 就有了类型推断功能,但是 80 年代中期的开发人员还无法接受这个概念,因此 Bjarne Stroustrup 不得不删除了 auto,直到 C++ 11 又重新添加回来。

从那以后,C++ 就不断加入各种工具来实现抽象。很难说 C++ 是一种低级语言还是高级语言。从设计目的上来说,C++ 两者都是。但是在不牺牲性能的情况下,建立高级抽象是很困难的。于是 C++ 引入了各种工具来实现 constexpr、move 语义、模板和不断增长的标准库。

从根本上讲,我认为 C 信任开发人员,而 C++ 信任编译器。这是一个巨大的差异,单凭 " 两者的原生类型相同 "、"while 循环的语法相同 " 等简单一致是无法掩盖的。

C++ 开发人员将有这些问题归咎于 C,而 C 开发人员则认为 C++ 过于疯狂。我觉得站在 C 的角度看 C++,这种说法也很正确。作为 C 的超集,C++ 确实很疯狂。一个经验丰富的 C 开发人员面对 C++ 可能没有熟悉的感觉。C++ 不是 C,这就足以引发互联网上的激烈争论。

然而,虽然我不喜欢 C,但也没有权利取笑 C。尽管我有一定的 C++ 经验,但用 C 编写过的代码少之又少,而且肯定是很糟糕的代码。好的编程语言包括良好的实践、模式、惯用写法,这些都需要多年的学习。如果你尝试用编写 C++ 的方式写 C 的代码,或者用 C 的方式编写 C++ 的代码,那感觉一定很糟糕。即便你懂 C,也不一定会 C++,反之亦然,懂 C++ 也不一定会用 C 编程。

那么,我们是否应该停止说 C/C++,为这两个不幸的命名而感到悲哀吗?也不至于。

尽管 C++ 的设计理念与 C 不一样,但是 C++ 仍然是 C 的超集。也就是说,你可以在 C++ 转换单元中包含 C 的头文件,这样依然可以通过编译。而这正是造成混乱的地方。

C++ 不是 C 的扩展,它是由不同的委员会、不同的人独立设计的标准。从逻辑上讲,喜欢 C++ 理念的人会参与 C++ 社区以及 C++ 标准化的过程,而其他人可能会尝试参与 C。无论是 C 的委员会还是 C++ 委员会,他们表达意图和方向的方式只能通过各自的最终产品:标准;而标准是众多投票的成果。

然而,编译器很难知道它正在处理的是 C 头文件还是 C++ 头文件。

extern "C" 标记并没有得到广泛一致的使用,而且它只能影响修饰,而不会影响语法或语义。头文件仅对预处理器有影响,对于 C++ 编译器而言,所有内容都是 C++ 转换单元,因此也就是 C++。然而,人们依然会在 C++ 中包含 C 头文件,并期望它 " 正常工作 ",而大多数时候也确实可以正常工作。

那么,我们不禁想问:

由不同地方的、不同的人开发的 C++ 代码如何保持 C 的兼容性?

恐怕很难。

最近,一位同事让我想起了康威定律:

" 设计系统的架构受制于产生这些设计的组织的沟通结构。"

根据这个逻辑,如果两个委员不互相合作,则他们创造的语言也不会互通。

C++ 维护了一个与 C 及其标准库的不兼容列表。然而该列表似乎并未反映出许多 C11 和 C18 中添加、但在 C++ 中不合法的功能。更清晰的介绍请参见这个维基本科页面(//en.wikipedia.org/wiki/Compatibility_of_C_and_C%2B%2B)。

然而,仅仅列出两种语言之间的不兼容性,并不足以衡量二者的不兼容性。

那些存在于 C++ 标准库中但主要声明来自 C 的函数,很难声明成 constexpr,更难声明成 noexcept。C 的兼容性会导致性能成本,而 C 函数是优化的障碍。

许多 C 的结构在 C++ 中都是有效的,但无法通过代码审查(如 NULL、longjmp、malloc、构造 / 析构函数、free、C 风格的类型强制转换等)。

在 C 看来,这些惯用写法可能问题不大,但在 C++ 中可不行。C++ 具有更强大的类型系统,不幸的是,C 的惯用写法在这个类型系统中凿了一个洞,因此实现 C 的兼容性需要在安全性方面付出代价。

别误会,C++ 仍然关心 C 的兼容性,某种程度上。然而,有趣的是 C 也很关心 C++,某种程度上。实话实说,C 对 C++ 的关心程度可能高于 C++ 对 C 的关心。看来,每个委员会还是在乎另一个委员会的工作。但我们很不情愿。

C++ 知道,许多基础库都是用 C 编写的,不仅包括 libc,而且还有 zip、png、curl、openssl(!)以及许多其他库,无数的 C++ 项目都在使用这些库。C++ 不能破坏这些兼容性。

但是最近,尤其是在过去的十年中,C++ 的规模已远远超过 C。C++ 拥有更多的用户,并且社区更加活跃。也许这就是为什么如今 C++ 委员会的规模是 C 委员会的 10 倍以上。

C++ 是不可忽视的力量,因此 C 委员会必须考虑不破坏 C++ 兼容性。如果非要说一个标准追随另一个标准对话,那么如今 C++ 是领头者,而 C 是追随者。

现在,C++ 处于稳定的三年周期中,无论是风雨还是烈日,抑或是致命的新疫情。而 C 每十年左右才发布一次主版本。不过这也很合理,因为作为一种较低级的语言,C 不需要发展得那么快。

C 语言的环境也与 C++ 完全不同。C 多用于平台,更多地用于编译器。每个人(甚至他们的狗狗)都会编写 C 编译器,因为该语言的特性集很小,所以任何人都可以编写 C 编译器。而 C++ 委员会真正考虑的实现只有四种,而且在每次会议上这四种实现都会出现。所以,C 语言中的许多功能都是与实现有关的,或者是可选支持的,这样各种编译器不需要做太多努力就可以声称自己遵从了标准,据说这样委员会的人会比较高兴。

如今,C++ 更加侧重于可移植性,而不是实现的自由。这又是一个理念的不同。

因此,你的提议破坏了 C 的兼容性

我提议的 P2178 的一部分理论上会影响与 C 的兼容性。这样的话所有方案都不会令人满意。

有人可能会说,你可以先向 C 委员会提议你的新特性。这意味着需要召开更多会议。C 会议的严格出席规则可能导致你无法参加会议,这就将那些不愿意花上数千美元成为 ISO 会员的个人拒之门外。这是因为 C 委员会必须遵守 ISO 的规则。

而且,如果新的标准刚刚发布,那么可能还需要等待十年时间,你的提案才会被考虑。最重要的是,如果 C 委员不理解或不在乎你正在努力解决的问题,那么你的提案就石沉大海了。或者他们可能没有精力来处理这个问题。而且,可能你也没有精力来处理 C。毕竟,你的本意是要改进 C++。实际上,哪怕会议上无人反对你的提议(尽管不太可能发生),如果有人让你先去跟 C 委员会的人讨论,就等于给你的提议判了死刑。

另一种可能的情况是,C 委员会接受与 C++ 中存在的版本略有不同的版本。true 只能做一个宏来实现。char16_t 需要通过 typedef。char32_t 不一定是 UTF-32。static_assert 对应的是 _Static_assert。

这类的情况还有很多,我们应该责备 C 吗?可能不应该。他们的委员会只是在尽力将 C 语言做好。反之亦然。在 C++20 中,指定的初始化器就受到了 C 的启发,但采取了略微不同的规则,因为如果完全一样的话就不符合 C++ 的初始化规则。

对于这个问题,我也有责任。C 有 VLA。如果当时我在,我一定会反对在标准 C++ 中采用它,因为它导致了太多安全性问题。我也会坚决反对将 _Generic 添加到 C++ 中的提议。也许 _Generic 的目的是减少由于缺乏模板或缺乏重载而导致的问题,但是 C++ 有这两个功能,从我的角度来看,_Generic 并不适合我想象中的 C++。

这两个委员会似乎对于对方语言的关心程度也不一样。有时我们会遇到兼容性非常好的情况(std::complex),有时完全不在乎兼容性(静态数组参数)。

这没有办法。别忘了每个委员会都是一群人,他们在不同的时间、不同的地点投票,而试图控制结果会导致投票毫无意义。将这些人放在同一个房间也不现实。ISO 可能会反对,参与者的不平衡会导致 C 的人处于极大的劣势。

C 的兼容性不重要

如果你是 C 开发人员,那么肯定会把 C 视为一种简洁的编程语言。但对于我们其他人而言,C 的印象完全不同。

C 是通用的、跨语言的胶水,可以将一切紧密地结合在一起。

对于 C++ 用户而言,C 就是他们的 API。从这一点来看,C 的价值在于其简单性。请记住,C++ 关心的那一部分 C 是出现在接口(头文件)中的 C。我们关心的是声明,而不是定义。C++ 需要调用 C 库中的函数(Python、Fortran、Rust、D、Java 等语言也一样,在所有情况下都可以在接口边界使用 C)。

因此,C 是一种接口定义语言。向 C 添加的内容越多,定义接口就越困难。这些接口随着时间的推移保持稳定的可能性较小。

那么,C++ 中缺少 <threads.h> 是否重要?可能并不重要,因为这不太可能出现在公共接口中。

如今大家都在谈论 C

过去,C 的兼容性是 C++ 的一大卖点。但如今,每个人(甚至他们的金鱼)都懂 C。Rust 可以调用 C 函数,Python、Java、一切语言都可以!甚至怪异的 Javascript 都可以在 WebAssemby 中调用 C 函数。

但是在这些语言中,接口是显式的。该语言提供的工具可以公开特定的 C 声明。当然,这比较麻烦。但这可以让接口非常非常清晰。而且还是有界的。例如,在 rust 中,调用 C 函数并不会迫使 Rust 牺牲某些设计来容纳 C 子集。实际上 C 是被包含进去的。

mod confinment {

use std::os::raw::{c_char};

extern "C" {

pub fn puts ( txt: *const c_char ) ;

}

pub fn main ( ) {

unsafe {

confinment::puts (

std::ffi::CString::new ( "Hello, world!" ) .expect ( "failed!" ) .as_ptr ( )

) ;

}

编译器资源管理器

除非 C 的 ABI 发生变化,否则这段代码可以一直正常运行。而且 Rust/C 的边界非常清晰、不言自明。

因此,C++ 可能是为 C 兼容性付出最多的语言。

更糟糕的是,打开任何 C 的头文件,你很快就会发现一堆 #ifdef __cplusplus。没错,C++ 的兼容性往往需要大量 C 开发人员的工作。兼容性一直是海市蜃楼。很多人都知道我的这条推文:

我们该何去何从?

我认为两个委员会都在尝试更多地沟通。他们计划明年在波特兰召开会议(尽管这个计划可能会变)。沟通是一件好事。

但是鸡同鸭讲的沟通效果会非常有限。两种语言的设计支柱可能都不协调。我会努力建议提供一个模板。但是首先我得吐槽 C 语言没有模块、没有命名空间,以及整个宏是什么玩意儿。

也许可以将 C++ 能接受的 C 子集约束在 C99 上?也许两种语言都需要找到一个共同的子集并独立地发展?也许 extern C 需要影响解析。如果 C++ 经历了多个时代,那么 C 可能是其中之一。

也许我们需要接受将 C 作为 C++ 的子集,但唯一的方法是将 WG14 融入到 WG21 中。

现状可能不会改变。C++ 可能永远也无法从自己的起源中解脱,而 C 可能永远都要与那些顶着 C 语言之名的肮脏特性战斗。

原文://cor3ntin.github.io/posts/c/

本文为 CSDN 翻译,转载请注明来源出处。

更多精彩推荐

谷歌软件工程师薪资百万,大厂薪资有多高?

CSDN 创始人蒋涛:选择长沙作 " 大本营 ",打造开发者中心城市

杜甫在线演唱《奇迹再现》、兵马俑真人还原……用 AI 技术打破次元壁的大谷来参加腾讯全球数字生态大会啦!

开放源码,华为鸿蒙 HarmonyOS 2.0 来了

20 张图,带你搞懂高并发中的线程与线程池!

跨链,该怎么跨?

点分享

点点赞

点在看