本文来自微信公众号: InfoQ ,编译:宇琪,作者:Tina
“我已经看到一些高级开发者开始退休了——因为他们不想再处理这种验证工作:你每次改代码、改提示词,生成出来的东西都会变。”75岁的C++之父Bjarne Stroustrup,在最新一期播客里说了这段话。
这句话很快在X上被转发、放大。有人把它浓缩成一句更适合传播的话:
“资深开发人员宁愿退休,也不愿处理AI生成的代码。”

严格说,这不是Stroustrup的原话。但它确实抓住了这段讨论里最让开发者有感触的部分:AI写代码的争议,已经不只是“它能不能生成代码”,而是生成之后谁来验证、谁来理解、谁来承担系统长期运行的后果。
有网友评论说,Stroustrup写系统代码的时间,比大多数AI工程师活过的时间还长。当他说“验证才是真正的问题”时,这并不是对变化的抵触,而是几十年来一次次看到系统出错、却无法追踪问题源头之后留下的经验。
“AI生成的代码更臃肿,有更多bug和安全漏洞,而且很难验证。”Stroustrup还指出,LLM用旧代码训练,生成的代码在“模仿旧代码,得到旧的性能和旧的bug”。
这期节目并不只谈AI。Stroustrup还系统回顾了他的职业生涯以及C++四十余年的演进脉络,编程语言的设计哲学、C++标准化历程。
他直言90%以上的内存安全漏洞并非C++语言本身的缺陷,而是“人们写的是C风格代码”;他透露C++的市场营销预算是三年5000美元,而Java的营销投入“比C++整个开发费用还多”;他回忆自己如何在IBM和Intel的工程师之间来回穿梭调解,最终促成一个被纳入C++11标准的技术妥协,而多年后双方都承认,他们实际使用的是双方方案的组合……基于该视频,InfoQ对内容进行了整理与部分删改。
核心观点如下:
先做到你能做到的最好,然后观察哪些有效、哪些无效,修复问题,再重复,C++就是这样成长起来的。
你必须先加固根基,确保它不是一堆随机功能的堆砌,因为那就是complexity。其次,不要妥协你的测试,这非常危险。第三,当高层老板说“必须做”的时候,他们不一定是对的。
他们想淘汰初级程序员,因为人数太多了。可你把初级程序员都砍了,将来又从哪儿去找资深程序员呢?
LLM用旧代码训练,生成的代码在模仿旧代码,得到旧的性能和旧的bug。
重要的不是学哪些语言,而是获得那些语言中蕴含的思想,你应该学和你当前语言不同的语言。
C++的起源故事
Ryan:C++的起源故事是什么?
Bjarne:最开始的起点是,我得到了贝尔实验室的一份工作,当时它是世界上最好的应用工程场所。我环顾四周,看到那里有那么多杰出的人:他们构建了Unix,构建了C语言,做了大量背后的理论工作。我意识到我必须做点重要的事情,否则我根本不配待在那里。
所以我决定要构建一个分布式Unix。因为很明显,计算机性能在提升,网络也在进步,我们需要那样的东西。如果我成功了,我们本可以提前10年拥有Unix集群。但当然,我一个人做不到,那不是一个单人能完成的项目。
但在这个过程中,我首先意识到的是“世界上没有一种语言能同时满足我的需求”,我需要两样东西:底层硬件访问能力——管理内存、进程调度、网络驱动、设备驱动;以及高层抽象能力——描述“这台计算机上的模块”和“那台计算机上的模块”以及它们之间的通信协议。有很多语言能做到其中之一,但没有一种能同时做到两者。
底层工作的明显选择是C语言,因为Dennis Ritchie(丹尼斯·里奇,C语言之父)和Brian Kernighan(布莱恩·柯林汉,C语言主要贡献者)就在走廊那头。我说要构建分布式Unix,是因为我就在Unix被发明、被持续构建的地方。而虽然高层语言数量不少,但它们都太慢了,而且不能操作硬件。但我学过Simula,我认识发明了面向对象编程和Simula的Christian Nygaard和Ole Johan Dahl,所以我决定把这两者融合起来。
可行的方式是把Simula中的类(class)概念拿过来,嵌入到C语言中,让它运行得更快,并用于系统编程。同时,我让类型系统变得更规整,用户定义类型与内置类型的处理方式完全一致。后来,为了支持泛型编程,我又必须添加重载(overloading),我需要把这种规则推广到用户定义类型和内置类型都适用。这就是C++的起点。
Ryan:在你的一次讲座中,你提到用BCPL重写了一个模拟器,那是分布式Unix的工作吗?
Bjarne:不,那是在那之前。我去英国剑桥大学读博士,某天我意识到需要一个分布式系统上的软件模拟器来完成博士论文,三四年后分布式Unix的想法也是从同样的思维方式中诞生的。我用Simula写了一个非常好的模拟器,它其实被名字耽误了,它是一个通用编程语言。
我写了模拟器,写了小例子和测试用例,一切都很顺利。然后我尝试了第一次真正的全规模运行,占用了系里的主机很长时间。博士生不能那样做——化学家、天体物理学家们绝不会接受,所以我被踢下了机器。我可以用Simula很好地编写程序,但我负担不起运行它的成本。
于是我把程序移植到了一台很少使用的实验计算机——CAP计算机上,它有硬件保护和能力机制,天体物理学家用不了,但计算机科学家可以。唯一的问题是Simula从未被移植到这种机器,它是专有的、只用在主机上,在CAP计算机的上下文里完全行不通。所以我用BCPL重写了模拟器,BCPL是一种会让C语言看起来都像高级语言的语言,它只有一种数据类型:word,那是一次非常痛苦的经历。
但一旦完成,我的程序运行速度估计快了大约50倍。我拿到了数据,拿到了博士学位。但我也坚信:我再也不会用那么不称手的工具去解决一个问题了。所以我列出了一份理想语言应该具备的特性清单,C++没有包含全部,但它比任何其他现存语言都更接近,C++就是从那里诞生的。
Ryan:在那次讲座中,你说用BCPL写那个程序太难了,你因此掉了一半的头发。
Bjarne:几乎完全正确。后来我为了把C++做起来,又掉了另一半。但不管怎样,它成功了。
传奇的贝尔实验室
Ryan:你提到了贝尔实验室,那是个传奇的地方。很多人对它充满好奇。当你博士毕业、考虑去哪里工作时,贝尔实验室在当时是个什么样的存在?
Bjarne:如果你想做大规模、世界级的实际工程,贝尔实验室就是首选。我们当时的计算机科学家数量,大概是MIT的两倍。
我在剑桥的最后一年,有个贝尔实验室的人来做讲座。英国计算机实验室的传统是:下班后去酒吧,跟人聊聊最近在做什么。那个人跟我说:“等你需要工作的时候,给我们打个电话。”于是我就真打了。我自费飞到新泽西,而我后来的上司Sandy Fraser告诉我,我来得不是时候,他们根本没职位。但第二天,我给一个开发团队做了报告。然后他们改变了主意,把我带到了研究组,我就在那里工作了二十年。
Ryan:面试过程是什么样的?
Bjarne:其实根本没什么正式的面试流程,就是跟一些人聊聊,他们已经五年没招过新人了,全靠直觉和口碑。我跟他们聊完,他们去找主管说:“这家伙不错。”我对此一无所知,当时还在加州跟人聊天,突然接到主管的电话:“你愿意来工作吗?”一周后我就去了。
Ryan:你哪来的信心自费飞过去?连工作都没保证。
Bjarne:因为那是世界上最好的地方,对吧?还需要更多理由吗?如果成了,那就是最好的结果;如果没成,那又怎样?你不可能事事成功。我也跟IBM聊了,去了Yorktown Heights的研究中心,跟他们的年轻研究员聊了聊,觉得他们做的事不对,方式也不对。IBM比贝尔实验室控制得更严、方向更死板。而贝尔实验室的研究文化,就是给你自由去解决真正困难的问题。
Ryan:像贝尔实验室这样的地方,项目选择是怎么运作的?你入职之后,他们怎么决定你做什么?
Bjarne:当时(现在可能也是)有两种研究哲学。一种是管理层精心设计一个项目,投入重金,派二三十个人去解决,然后搞出个大东西。另一种哲学是:招最牛的人,然后别告诉他们该干什么。我的工作描述就是:“做点有趣的事”。一年后,告诉我们你做了什么。如果我们喜欢就续约,明年同样条件。而且,汇报方式是一页纸,字号不能小于9磅,因为你如果连自己做了什么都不能简短说清楚,那大概率没做出什么真正有趣的东西。
所以当UNIX团队壮大到五到七个人时,管理层就开始担心了,因为这个规模已经超出了“个人做有趣的事”这个模式的承受范围。这种近乎无政府主义的组织,产出比那些精心规划的团队要好得多。你听说过的贝尔实验室成果,大部分都出自那里。同一栋楼里还有做硬件的——我们今天用的光纤、大量无线技术、摄像头里的电荷耦合器件,都来自那里。他们试过视频电话,硬件跟不上没做成,但他们在尝试。手机蜂窝系统则出自另一栋楼。
计算机科学的人会经常和其他领域的人交流。我记得做仿真时,帮别人搭网络仿真器。早期C++很多工作都跟网络过载、过载协议怎么处理有关。有一次他们做得不错,又找我,说想仿真曼哈顿的计算机流量。我的回答是“不”,当时的算力根本做不到。不管C++多好,当时的计算机就是不行。现在也许可以了,但流量也变了。
C语言之父与胖指针
Ryan:我听说你每周和Dennis Ritchie吃一次午饭,持续了16年?他对你有什么影响吗?
Bjarne:他是个了不起的人,我们聊了很多。他从来没说过C++的坏话,实际上,在他关于哈勃望远镜的论文里,他把C++列为C的当然继承者。所以那些C和C++之间的语言战争完全是荒谬的,根本不该发生也没发生过。我还认识Brian Kernighan,上周五刚跟他聊过。我们是好朋友,语言战争很愚蠢。
Dennis帮我设计了C++的const。它最初叫"read only"和"write only",但C语言那帮人觉得两个单词太长、搞不定,所以我们最后变成了现在这样。他对重载有点担心,因为你要先看函数的声明才能知道调用是什么意思。这种担心很合理,只是事实证明它能工作。而且现在写C语言的人其实每天都在用我的成果:现代C语言里函数定义和声明的语法,以及核心语义,都来自早期C++的工作。所以当人们开始骂战的时候,应该记住他们每天用的都是我的作品。
Ryan:你写过三篇很长的C++历史论文,里面提到Dennis Ritchie向C标准委员会提过一个“胖指针”的概念——指针里同时存储大小,但C委员会没批准。能讲讲这个故事吗?
Bjarne:我们早就知道缓冲区溢出和范围错误的问题,最明显的解决方案就是使用所谓的“胖指针”,即一个指针加上它所指向的元素数量。但那意味着指针变成两个字长,这在早期C语言中根本行不通,因为当时内存只有48K。
当我跟Dennis讨论这个问题时,我们大概有1兆字节的内存。而当我开始做C++时,只有256K,但我已经知道我们会拥有1兆字节。他管这些叫“胖指针”,而今天在C++里,它们被称为span。span来自我和其他人一起做的C++核心指南工作。我们需要类似的东西,否则无法提供所需的控制和安全性。所以我们构建了span,它后来进入了C++标准。但胖指针的想法真的很古老。
Ryan:当你把一群真正令人印象深刻的人聚在一起,其他人的卓越会让人产生一种冒名顶替综合征的感觉,你曾经感受过或目睹过这种情况吗?
Bjarne:绝对有,也许我现在还有一点这种感觉。当我来到贝尔实验室,看到门上的名字,我读过他们的论文,他们创造了我喜欢工作的领域,我当时就想,我必须提升自己的水平,我必须做一些比我之前想象的更大、更好的事情。那里有一群人讨论他们在做什么以及为什么这么做,你和他们交谈,你会学到东西。剑桥大学的计算机科学系也是这样一个地方,我在那里学到了很多。而且门总是开着的,这几乎成了一条不成文的规定:我们保持门开着,否则别人怎么进来和你交谈呢?
语言设计哲学
Ryan:说到语言设计,如果今天有人想从头构建一门编程语言,需要准备哪些东西?
Bjarne:这个问题其实很容易回答,但我觉得这是个错误的问题。真正需要问的是:你需要解决什么问题?很多人只是想造一门语言,比他们现在用的更好一点,专门针对他们正在做的事情。但大多数情况下,现有语言已经能做得不错了。如果你造一门非常专门的领域语言,那没问题。但如果是通用语言,情况就完全不同了,当你需要和别人协作时,你的专用语言对别人来说并不理想。
所以有人问为什么C++那么庞大和复杂,答案就在这里。有两个原因。一个是历史。我在80年代无法构建出我理想中的C++,原因很多:技术限制、计算机性能不够,还有我当时懂得不够多,我必须边学边做。这就是标准的工程做法:先做到你能做到的最好,然后观察哪些有效、哪些无效,修复问题,再重复,C++就是这样成长起来的。所以确实有一些遗留问题,至今还在碍事。比如现在我们有span,你很少需要直接用指针,更不应该把指针当作资源句柄,这个理念早在1979年就用C with Classes实现了,但人们当时没理解,所以还需要持续的教学。
一旦你确定某个问题确实需要一门新语言来解决,那就开始调研现有的工具。现在有很多帮助:分析类的书、代码生成的框架,比如LLVM。大多数现代语言都用C++的基础设施来生成代码,这非常有趣。但关键是:聚焦于问题本身,别以为自己是唯一的用户。如果你只想着自己,你造出来的就是一门专用语言。当你找到合适的问题和解决方案时,领域特定语言很好,但首先得识别出问题。
拿我来说,我当时的问题是:我需要在同一门语言里同时拥有高层和低层设施。否则我就得用两门语言,还得让它们正确通信。而当时的高级语言往往通过接口牺牲性能,而且经常需要垃圾回收,这对设备驱动或构建垃圾回收器本身来说并不理想。所以,先识别问题,再尝试解决。
Ryan:当你发现问题后,开始构建C++时,有很多组件——编译器、链接器、解析器、词法分析器等等。在最初实现中,哪部分技术挑战最大?
Bjarne:我不觉得哪部分特别难,更多是部件太多。我做了一个决定:不动链接器。原因很简单,我调研后发现,贝尔实验室内部大约有25种不同的链接器。如果我要服务最初的用户,就得为25个链接器写接口或修改方案。没人愿意让你碰他们的链接器,因为一旦出错,整个系统都会崩。后来人们确实改进了链接器,但那是C++成为大问题之后的事了。
另一个问题是优化器,每台计算机来自不同厂商,各有各的优化器,我写不了一打优化器。我的目标不是成为优化器专家,而是先构建一个分布式系统。当朋友和同事开始用我的类时,我想帮他们,让他们做网络仿真、硬件布局、卫星定位,各种有趣的事。而且我发现,所有这些优化器和代码生成器有个共同接口——C,那就用C吧,C在底层方面非常出色,这也是我选它的部分原因。
当时我说:我们可以保留Dennis的已知错误,再加上我未知的错误。这样更可控、更可理解,我也不用教人怎么写for循环之类的。C兼容性就这么来了,一半是作为实现技术,一半是为了融入已有的文化和工具支持。
Ryan:我注意到C++工具链的某些部分是用C++本身写的,这就有个鸡生蛋蛋生鸡的问题。怎么做到的?
Bjarne:这叫自举(bootstrapping),并不罕见。我从C开始,用C写了一个预处理器,用来实现一些后来成为C++类的基本机制,包括继承、重载之类的特性。接着我又用这个版本写了一个简单的编译器,它处理的依然只是C++的一个子集。这时候我已经能使用运算符重载和一般的重载,还有类了。所以我就可以构建一个作用域类来处理查找、命名这些事情。然后就从这个基础上继续推进,一直用上一个版本来写下一个版本,这样持续迭代,几年之后就有了后来被世人称为C++的东西。我为此写了一本书,编译器也跟着问世了。
Ryan:很多人以为C++是纯面向对象语言,但你反复强调不是这样。
Bjarne:我从未称C++为面向对象编程语言。在《C++程序设计语言》第一版里,最接近的说法是“有些人把这些技术称为基于对象(object-based)”。实际上,C++更注重类型导向(type-oriented)和类导向(class-oriented)——它确实很好地支持了面向对象技术,尤其继承了Simula定义类型、定义类、用类层次结构处理相关类群的模型,但这从来不是它的全部。
举例来说,我绝不想要面向对象的复数。我不想写2.as…()去访问数字的某个部分,我想要的是2+z,而且它应该和z+2几乎一样快。数学在过去的300多年里已经发展出了一套极好的记号法,笛卡尔大概是第一个使用这种记法的人。所以我不希望所有东西都被封装成对象。
更进一步,我要求那些不需要继承、不需要运行时解析的东西,就不要用这些机制。对于算术运算和复数这类东西,我需要的是Fortran兼容性。当时很多研究者想构建一套全新的语言和系统来实现复用,但我的视角不同,我想复用已经存在的东西。Fortran有优秀的数值软件,C有出色的系统软件,Simula也被广泛使用。我不能离硬件太远,不能去构建那些“理论上理想”甚至“我自己认为理想”的东西。这是真实世界,你要面对的是真实问题,你必须尊重约束。
Ryan:你在设计C++时,C已经存在,而它的类型系统比C++弱得多。为什么你选择让C++的类型系统更强?
Bjarne:因为我们需要它。类型系统的薄弱是错误最明显的来源之一,也是无休止测试和debug的根源。我讨厌debug,我宁愿做设计。我想把天平更多地倾斜向设计,这有助于减少debug,减少运行时的错误,类型系统就是其中一种手段。
实际上,今天C语言的类型系统已经比当年强大了很多,部分原因正是C++的影响。而且,如果没有强类型系统,有些东西你根本表达不出来。比如我之前提到的重载。重载对于泛型编程至关重要。如果你想写一个vector
贝尔实验室在芝加哥做过一项大规模实验:一些团队从C切换到C++,人们想知道他们是否更高效。有人抱怨编译速度变慢了。于是有人简单测量了切换前后的编译时间,结果发现计算能力消耗总量大致相同,当时C++编译速度慢了大约两倍,但C程序员编译的次数也是两倍。这只是一个实验,但趋势很明显:我想把更多工作推向编译时解决。
Ryan:C++是有名的静态类型语言,为什么你选择了静态类型?
Bjarne:想象一下,在Smalltalk这样的动态语言中,运行时出错时你会进入调试器。这在程序员坐在屏幕前看到错误时很有用,但如果是电话交换机遇到运行时错误,你没法派一个程序员去现场调试。此外,你需要性能,需要程序能塞进小内存里。今天依然如此,我认为99%的计算机是嵌入式系统,它们内存受限。如果你做运行时解析,就需要保留足够的信息和数据来支持它。而我设计的系统要能塞进100KB、120KB、250KB、1MB这样的内存里。今天相机、手机、摄像头仍然内存受限,静态类型语言在内存优化上天生更好。
Ryan:你刚才提到内存受限的问题,这让我想到静态类型和动态类型的另一个维度——开发效率。对于普通开发者来说,哪种方式更省时间?
Bjarne:这个问题其实没有定论。静态类型语言的好处是编译器在你组装最终产品之前就对你大喊大叫,错误发现得更早。而动态类型语言中,错误可能到运行时才暴露。JavaScript和Python很流行,但它们运行时检查,跑起来慢得多。纯Python比纯C++慢大约70%。为什么它们还能用?因为关键库都是用C或C++写的,靠底层语言来获取性能。这又回到了我的起点——你需要高层抽象,也需要能操控硬件的底层能力。这里用了两种语言,但根本需求是一样的。
在动态类型语言里尝试新想法确实更容易,因为你不需要了解类型系统。但问题在于,静态类型语言中由类型系统发现的错误,在动态类型语言中要到运行时才暴露。随着系统变大,性能问题开始显现,你会发现写出可靠软件越来越难。动态类型语言需要多得多的单元测试,因为编译器不帮你干活。如果你要保证东西不出问题——比如电话交换机不能崩溃、你的车不能崩溃、你的飞机不能崩溃——你就需要保证,而在非常灵活的动态类型系统中,这种保证很难提供。
Ryan:说到保证,C++在内存安全问题上名声可不太好,那些“自爆开关”(foot guns)让很多人头疼。
Bjarne:我对这个说法已经厌倦了,我已经很多年没遇到这些问题了。有人做过研究,分析缓冲区溢出和黑客利用这类漏洞的攻击,超过90%的案例都是因为人们写的是C风格代码,或者直接写C。Herb Sutter(C++标准委员会主席之一)有一个演讲给出了实际数据,非常触目惊心。这些人写的压根不是“现代C++”,他们用原始指针传递东西,却不传递元素数量,不用胖指针,不用span。C++里有这些工具,你可以用vector,我们有加固库——苹果有、谷歌有、微软有,但之前一直不是标准。C++26已经包含了标准的加固选项,而我在做的Profiles工作会提供一种方式,保证你不会做那些蠢事。理论上,这个问题很多年前就解决了,但人们就是沿用老做法,于是得到老问题。这让我很伤心,也是我致力于编码指南、强制Profiles和教育的原因。
Ryan:有没有办法让编译器直接阻止人们做那些危险的事?这在现代C++里默认开启了吗?
Bjarne:没有,但应该开启。我提议在C++29中实现。更简单的版本本来应该放进C++26,但C++标准委员会里仍然有很多人非常忠于他们的旧代码和老做法。有人说“你应该只标准化业界通用的东西”,但当bug在业界普遍存在时,你就该做点别的了。
C++标准化历程
Ryan:标准委员会是一个有趣的话题,语言由一个民主机构管理。如果它是一个独裁体制,你一个人说了算,你会加入哪些语言特性?
Bjarne:它从来就不是独裁。一旦你有了用户,在我看来你就有了责任去帮助他们,确保他们的代码能工作。你不能一直破坏语言,那是学术语言开发的做法,他们为了改进不断破坏,然后无法维持用户群体。我其实没有主动选择成立标准委员会,我选择了对社区负责。
但有一天,两个家伙走进我的办公室,代表IBM和HP,当时世界上最大的计算机和软件供应商。他们走进我的办公室,那是1989年,他们说:“Bjarne,我们希望你在ISO规则下标准化C++。”我说不行,我还在做实验,语言还不完整。他们说:“Bjarne,你不明白。我们的组织不能使用没有标准化的语言,也不能使用由我们可能竞争的公司拥有的语言。我们信任你,但不信任你的雇主。我们有时和AT&T竞争,你可能会被老板压垮。”他们就这样“绑架”了我大约一个小时。最后我说:“好吧,我会按照你们的提议,在ANSI规则下标准化C++。”
Ryan:如果你当时坚决说“不”呢?会发生什么?
Bjarne:那C++可能就会慢慢淡出,变成一门学院派的、精致的小语言,被一个小圈子喜爱,然后从主流计算中消失。有些人比我说得更直白:C++之所以能广泛传播和使用,正是因为它有标准,它不属于任何一家公司,这也是为什么那些想杀死C++的语言总是失败的原因之一。我记得Java的广告里有人说“我们会在两年内彻底干掉C++”,我觉得这很粗鲁。结果呢?今天C++开发者数量是当时他们放话时的10到12倍。
Ryan:光看采用率的话,C++显然比Java高得多。可我知道Java背后有大公司撑腰,砸了那么多营销的钱,而C++呢,我记得你好像说过它的营销投入几乎为零。
Bjarne:几乎是零,差不多是5000美元,分三年用。而Sun在Java营销上的投入比C++整个开发费用还多。直到今天,标准委员会都面临资金问题,没有政府资助,很难做实验和部署新特性。其他语言社区还不断挖走C++编译器和工具开发者,因为他们确实很优秀。
我上次查的时候,C++标准委员会有527名成员。我们靠共识工作——如果不达成共识,就会出现“方言”。我们不会接受一个60%对40%投票通过的特性,更别提52%对48%了。我们不需要100%全票,但需要压倒性多数,我理想中是90%,80%时我开始担心。
Ryan:规则里写的最低底线是多少?
Bjarne:没有最低底线。ISO委员会的召集人决定什么是“共识”。想象一下,95%对5%的投票,但反对的5%恰好是来自Google、Apple、Microsoft等公司的编译器实现者,没有哪个召集人会认为那是共识。
Ryan:民主决策是不是总得有点客观规则才行,万一召集人判断错了怎么办?
Bjarne:这种事确实会发生,但你不能只靠数字规则。因为不是每个人都关心整个语言,也不是每个人都理解全局。你可能第三次开会就有了投票权,所以也许有个有投票权的人,标准化经验只有八个月,压根不懂标准化,只了解自己所在的小开发组织。你需要判断力,希望召集人有这种判断力。召集人会咨询各国代表、实现者、教育者,在分歧出现时,每个人的权重并不相同。
Ryan:你曾说过你提过的所有想法里,被怼得最惨的之一就是auto。我知道auto后来还是进了标准,但当时它为什么那么不受待见?
Bjarne:当时太反常了,人们认为它会削弱类型系统。而且它打开了通用泛型编程的大门,不需要复杂的语法。auto是concepts(概念)的开始,它是对泛型代码施加约束的能力,auto就是最简单的约束:它必须是一个类型,而不是一个值。可能我当时解释得不够好,而委员会的背景又太多样。不过最终,人们逐渐理解了auto的价值,它成了现代C++最受欢迎的特性之一。
我调研过工业软件,发现auto被过度使用了,你应该只在清楚需要什么类型时才用auto。在泛型代码中它很好用,你一路写下去,最终会检查auto解析到的类型是否支持你打算做的操作,这正是concepts试图形式化的东西。但我发现有一群人,实际上是在一个网络框架中,过度使用auto。他们说自己被拖慢了速度,不是因为有bug,而是他们不得不去查看被调用的函数,才能搞清楚那个auto到底绑定到了什么类型。然后他们还得在注释里写明auto的意图。比如你写auto,注释里写“必须是一个输入通道”。那为什么不直接定义input_channel,然后写input_channel auto呢?这就是我的设计思路:auto是最简单的concept,你完全可以说input_channel auto c=...。但委员会想要一个明确的指示器来表明这种约束正在进行。
Ryan:我还听说委员会内部有时会激烈争吵,比如你做过“穿梭外交”,在IBM和Intel的两拨人之间来回调解。
Bjarne:是的。IBM当时用的是PowerPC架构,Intel是x86,他们对底层硬件(尤其是缓存协调)有不同的模型。而且IBM的代表告诉我,他们有一大堆底层软件甚至微码级别的,都依赖他们一直以来的做法,而写那些代码的人早就离职了。他们根本没法重写全部代码并保证正确,哪怕Intel是对的。Intel那边也有类似的说辞。于是两拨人坐在一个大房间的两个角落,完全僵持住了。我花了几个小时,从Intel代表那边走到IBM代表那边,再走回来,反复解释对方的立场和顾虑。最终我们达成了一个协议,这个结果被纳入了C++11。几年后,双方都承认他们现在实际使用的是双方方案的一个组合,这个结果是一次跨领域的改进。
Ryan:1995年你曾提议在C++中引入某种形式的自动垃圾回收(garbege cllector)。这让我很惊讶,因为C++没有垃圾回收器,我们得自己手动管理内存,这到底是怎么回事?
Bjarne:第一,我想自动化的是资源管理,而不仅仅是垃圾回收或内存管理。为此我们有构造函数、析构函数,以及后来被称为RAII(资源获取即初始化)的技术。第二,标准委员会里有些人坚持我们需要支持垃圾回收,而且当时已经有垃圾回收器了。Hans Boehm就是代表,他有一个保守式垃圾回收器,至今仍在使用。我们认为需要一个标准接口,这样人们可以统一使用垃圾回收器。所以我是在倾听专家用户的意见,他们认为这是必要的。
经过长时间的讨论,我们找到了一个大家同意的接口,把它放进了C++11。但接下来十年里,我们发现垃圾回收的使用量在下降,因为RAII,这种一直存在的资源管理方式,被越来越多的人理解和采用。而且,那些仍然使用垃圾回收器的人也没有用标准接口,因为他们找到了更好的方式。所以今天还有少数人用垃圾回收,但它已经不是标准的一部分了。
Ryan:那个标准接口是怎么工作的?是对内存分配方法做了一层包装?
Bjarne:对。你对底层的`new`、`malloc`或`operator new`做不同的实现,然后`delete`也变得稍微不同。
不要在脆弱的根基上堆砌功能
Ryan:C++社区里有一个关于“瓦萨号”战舰的警示故事。为什么它这么流行?
Bjarne:如果你去斯德哥尔摩一定要看看瓦萨号,它是17世纪的一艘战舰,故事是这样的:国王下令建造一艘最好、最漂亮的战舰,既能打仗又能用于外交访问。他们铺好龙骨开始建造,然后听说一个潜在的对手正在建造双甲板战舰,而瓦萨号只有单甲板。如果单甲板对上双甲板,结果可想而知。于是国王下令:这艘船现在要有双甲板。他们已经建了一半了,又加了一层甲板,加上大炮。国王还想要更多雕像和装饰品,船变得头重脚轻。他们测试稳定性时,让全体船员从一边跑到另一边,来回跑,产生谐波振荡。如果能跑14次,就能扛住波罗的海和北海的风浪。结果,他们跑了7次就停了,因为看起来太危险了。1624年,船完工了。它驶出斯德哥尔摩港,号角齐鸣,旗帜飘扬,船员家属都在船上。它开到港口中央,一阵风吹来,船倾覆了,沉入海底。
我把这个故事讲给标准委员会听,指出他们做错的事:他们在没有加固根基的情况下堆砌了太多功能。你必须先加固根基,确保它不是一堆随机功能的堆砌,因为那就是complexity。其次,不要妥协你的测试,这非常危险。第三,当高层老板说“必须做”的时候,他们不一定是对的。有时候专业人士应该说“不,我们不能这样做。我们得慢慢来。”如果他们当时说“好吧,我们造一艘单甲板战舰,不叫瓦萨号,叫个别的名字。明年我们再从头设计一艘双甲板战舰”,那就什么问题都没有。但高层管理,当时在波兰的国王,说“必须按时交付”。于是他们按时交付了一艘根本不能完成任务的船。
Ryan:有人证明C++模板实例化机制是图灵完备的。编译器在预处理C++程序时,实际上可以用它来做计算,比如有人用它在编译时计算素数。这怎么做到的?
Bjarne:素数那个例子只是个噱头,它用错误信息来报告结果。要实现图灵完备,你需要某种形式的迭代或递归,还需要比较,这就够了。一些理论家说“这不行,程序会永远跑下去”。第一个做出这个例子的人认为我应该禁止它,不过我的反应是:这看起来很有用。而且,没有任何东西会永远跑下去。图灵机需要无限长的纸带,但真实的机器是有限的。关键是编译器在遇到真正的问题之前就会耗尽资源,所以这不是问题。
但真正发生的事情是,人们开始滥用模板去做一些简单的计算,比如素数、阶乘之类的。那东西太可怕了,开销极大,要吃掉海量内存,这就真成了问题。这就是为什么我和Gabriel Dos Reis一起设计了constexpr,它让你可以在编译时用普通代码做计算,更简单、更熟悉、编译更快,通常生成更快的代码。constexpr解决了模板被滥用于计算的问题,把它变成了普通函数。
Ryan:一般来说,大家对编程语言有个直觉:你离机器越近,性能就越高。我常听人说,C比C++更贴近机器。
Bjarne:不是这样的。我们有完全相同的机器模型——C借用了C++11的机器模型。如果你用两种语言写同样的代码,结果是一样的,除了C++编译器可以在编译时做更多事情。所以C++在大多数情况下和C一样快,甚至更快。如果你给优化器更多信息,它能做得更好。
Ryan:你好像在哪说过C++可以比C性能更高,但我总觉得更多的抽象总得付出点代价吧。
Bjarne:它在编译时被优化掉了。这就是我所说的“零开销抽象”。现在有人开始批评我,说这个说法低估了C++编译器的能力——我们可以实现“负开销抽象”。
Ryan:如果我是一个汇编高手,有无限的时间,那比起来怎么样?
Bjarne:如果你非常聪明,有无限时间,你可以做得更好。但我们大多数人没有优化器聪明,也没有无限时间。即使你很聪明,也只能优化一小段代码。我给金融行业的人做过一个演讲,标题是“不要太聪明”。我的主要观点是:C++对98%以上的代码已经足够好了,如果你想把时间花在“聪明”的优化上,先用现代C++写出干净的代码,这样你才有时间去处理那2%真正需要优化的地方。而且,聪明的优化现在往往是机器相关的,换了新机器或新编译器,你的“优化”可能反而拖慢速度。
我从八十年代起就反复看到这种事。有些人什么也不干,就专在新一代硬件上测试各种优化。而我改善性能的标准手法,其实是先把那些聪明玩意儿全扔掉,再看看跑得更快了还是更慢了。通常,你会跑得更快,因为那种聪明玩意儿,尤其是在人们脑子里根深蒂固的九十年代风格,很多至今还留在代码里,往往喜欢用密密麻麻的指针网,这会给编译器和优化器带来大麻烦。它们有时候还会做更多的内存分配,这也不好。你要尽量减少内存访问,最大化缓存性能这些东西。
我跟一个在西班牙做流体动力学的朋友合写了一篇论文,我们拿一个性能测试套件里的真实例子,把那些聪明玩意儿全扔了,结果代码量减少到原来的百分之八十,性能反而提升了百分之二十。你只在需要的时候才做优化。Knuth说,不要过早优化,但他也指出,只有那百分之二到三的地方才值得去优化。所以,先用高层次设施把东西搭起来,看看够不够好;如果不够,你必须去测量时间,找出时间都花在哪儿了,最后再优化那部分。但很多时候,你根本走不到那一步,它已经足够快了。
Ryan:你说的“聪明”,是指靠人工手动管理去榨取性能,而你的意思是,如果不去做这些,编译器反而能得到更多信息去优化,而且它现在干得远比以前好。
Bjarne:那些在九十年代被精巧而正确地优化过的代码,放到今天常常变成劣化,因为机器架构变了,编译器也进步了。
AI写代码越多,高级工程师越不想接盘
Ryan:如果越来越多的代码是由模型和机器写的,你觉得编程语言设计会跟着变吗?
Bjarne:我觉得,在我最感兴趣的那个领域里,代码仍然会由人来写,而且人会使用抽象。我看到过一些尝试,让AI在这个领域生成代码,但这些尝试并不成功。它们会生成更多bug,更多安全漏洞。它们生成的代码也很臃肿,这反过来又会拖慢性能,因为你用了更多内存,而且这些代码很难验证。
要验证它们,需要高级开发者,但我看到有些高级开发者已经开始退休了,因为他们不想再处理这种验证工作:你只要改一下代码、改一下提示词,生成出来的东西每次都会变。
而且,我思考的很多东西还涉及监管机构,涉及验证。你做了一个改动,就必须能够验证你到底改了什么。但AI、这些工具本身会变化;即使你只是稍微换一种提示词,很多代码也会变化,然后你又必须重新检查一遍。所有生成出来的代码都要检查,而且你要知道,生成出来的代码量通常会比人类写的更多。
一个人做改动时,通常会做一个局部改动,你可以去看这个局部改动带来的影响。但如果是AI写的,嗯,你其实不知道它到底改了哪里,你还得自己想办法弄清楚。
所以,如果你做的是那种已经被做过很多次的东西,比如写一个标准Web应用,那你说的是对的。
嗯,AI也不是没用,我不是这个意思。它可以用来写文档。当然,文档也必须由人来验证,但它确实能帮忙写东西。它擅长文本。至少现在,它并不擅长写安全关键、性能关键的代码。
现在,假设全世界70%或80%的代码都不属于这种类型,但我感兴趣的是那10%到20%的代码。在这一部分,它还没有做到,而且我也看不到它会通过LLM这种模型做到。
进一步说,LLM在喂入训练数据时,必须用旧代码来训练。而我理解自己的工作,是确保人们能写新的东西,使用新的技术,写出比旧代码更好的东西。所以我发现,基于LLM的代码是在模仿旧代码,得到的是旧性能和旧bug。
当然,也许你可以改进它。我听说有人在写Bjarne app,喂给它我的文章,但就算那样也有问题,因为我现在说的东西,并不完全等同于我20年前说的东西。
不过不管怎样,我们会看到结果的。嗯,甚至Dijkstra也研究过这种可能性,他当时声称,把自然语言当成编程语言的想法是愚蠢的。他没有我这么客气。
我的看法是,像英语这样的语言非常灵活,我们说的话往往非常含糊。但我们需要的是一种精确的编程语言,那是工程,是数学,不是英语。
Ryan:对于那些性能攸关或安全攸关的代码,我猜还是会有一小群人尝试用LLM去弄。按你说的,那会导致更多的崩溃和bug,因为它是没经过验证的。
Bjarne:那些真正擅长这种事的人,往往又不愿意把所有时间都花在验证上。还有个问题是,他们想淘汰初级程序员,因为人数太多了。可你把初级程序员都砍了,将来又从哪儿去找资深程序员呢?我们等着看吧。
AI的支持者总是说“问题已经解决了”或“下一个版本就会解决”。但我听说Anthropic 4.7比4.6有更多问题,原因我不理解。
不过,“下一个版本会解决这个问题”这个想法,本身始终是一个危险的假设。更进一步,它也变得越来越贵。如果你必须建一个价值1亿美元的数据中心,还要支付运行它所需的电费,那么到底需要多少初级开发者,才会比它更便宜?嗯,它们现在开始需要钱了。这并不是没有问题。而我所在的这个子领域,可能比大多数领域都更容易遇到这些问题。
给初学者的建议与反思
Ryan:有人问你是什么支撑你一直搞C++,你说,一是构建未来的那种乐趣,二是觉得自己有义务确保C++不断前进。当你刚开始做C++的时候,我很难想象你知道自己会踏上一条长达几十年的旅程。
Bjarne:倒不是几十年,但我知道这会是条很长的路,因为我知道我没法一步到位造出我想要的语言,只能先造一个子集。原因有两个:第一,我是一个人在做这件事;第二,我缺少足够的反馈输入,来确保我设计的东西是对的。这就是工程上的问题了:尽你所能去建造,看看什么管用,然后再改进。所以我知道我在创建一门注定要持续演进的语言,而“注定要演进”意味着,你在做某些决定的时候,就已经知道这一点跟别的不一样。举个例子,这也是C++不只是一个面向对象编程语言的原因之一,因为我当时看到了很多东西,似乎无法塞进那个范式里,所以我知道我们会演进。
支撑我走下去的另一半答案,是那些应用。看到那些有趣的用法,感觉真是太棒了。比如我去喷气推进实验室,跟搞火星车的人聊天,我还去过欧洲核子研究中心,去看他们怎么搞高能物理的。几年前,我跟一个哥们儿聊天,他的工作是开门和关门。那些门有几吨重,是铅做的,会横向移动,去封闭某个区域以防辐射什么的。启动这扇门倒不难,有引擎。但他必须确保能把它停住,因为当几吨重的家伙向一堵墙移动时,它可不会自己乖乖停下。他得写代码来处理这个,那段代码非常有意思。我至今仍在到处旅行,跟人聊天,看C++正在被用在什么地方,这也是学习,而学习本身就很有趣。
Ryan:你有一个著名的比喻:C让你容易射中自己的脚,C++让你更难射中,但一旦射中,整条腿都没了。
Bjarne:对,这来自我80年代在波士顿的一次演讲。Arnold Penzias(诺贝尔物理学奖得主)曾试图向一群贝尔实验室的经理解释C++。他说:“你不能在不了解如何使用的情况下使用电动工具,你用手锯,可以这么来回锯。可你要换了把电锯还这么干,它会直接弹起来,你要是不受伤那就算走大运了。”背后的意思就是,你拿到一件更强大的工具,却用错了方法,惹出的麻烦只会更大。
Ryan:你还有一句名言:“没有人应该自称专业人士,如果他们只懂一门语言。”你显然会建议大家学C++,但如果他们为了成为更好的工程师或程序员,必须再学一门第二甚至第三语言,你会推荐什么?
Bjarne:重要的不是学哪些语言,而是获得那些语言中蕴含的思想,你应该学和你当前语言不同的语言。具体挑哪种,我其实不太纠结。我当时好像说去学一门脚本语言,今天可能是Python或JavaScript,在那时候大概是Unix Shell之类的。再看看一门函数式语言,ML或Haskell是明显的选择。关键是不要只局限在你的语言里。就像那个笑话:懂三种语言的人叫“三语者”,懂两种语言叫“双语者”,懂一种语言叫“美国人”。编程语言也一样,但我觉得编程语言更重要,因为你是在构建东西,你应该用不同的思想和技术来拓宽你的视野。
Ryan:还有一个:“那些自以为无所不知的人,真的很烦我们这些自知一无所知的人。”
Bjarne:有太多人认为世界上所有事情都有简单的解决方案。在这个语境下,他们会来告诉我C++可以简单多少。确实,如果你只想做一件事,你可以做一门简单得多的语言。如果你扔掉C++的一部分,它会更简单、更漂亮,通常他们想扔掉的是跟C兼容的部分。但那样你会惹恼几百万人,而且不会成功,因为他们会继续抱着旧东西不放。所以,这不过是我对那些过度简化事物的人表达挫败感的一种方式罢了。有些人觉得不用学编程就能写程序,不用学工程学就能当工程师,不用懂怎么管理公司或国家就能当政客,这种过度简化让我恼火。
Ryan:你说过C++不是为所有人设计的,而是为严肃的程序员准备的。
Bjarne:对。《C++程序设计语言》第一版的第一行是:“C++旨在让严肃程序员的生活更愉快。”第一版是的用词是“专业程序员”,后来改掉了,因为我见过非常优秀的业余爱好者。“严肃程序员”大概是为别人编程的人。如果你为自己编程,没关系,那是你自己的问题。如果你为朋友编程,你可能会失去朋友。如果你为一百万人构建东西,你可能会对世界造成伤害。Guido van Rossum设计Python的明确目标是让更多人,甚至每个人都能编程,他成功了。我设计C++是为了给严肃的程序员、工程师、数学家,提供真正好的工具,我也成功了。我们解决的是不同的问题。
Ryan:回顾C++的整个旅程,有没有什么地方你觉得是错误,或者学到了教训?
Bjarne:很多很多次。但大多数错误从未进入C++——这就是你需要做实验、需要早期试用的原因。我认为语言的核心部分是对的,但每一个细节都可以改进。然而,稳定性和兼容性才是根本。你要是做个小改动,会惹毛一小部分人,影响不大;可你要是做了个大改动,你就会惹毛一大群人,而且根本行不通,因为比如会有上百万人坚守旧的方式。所以我努力的方向是,让这门语言在不破坏现有东西的前提下生长。
有个场景我反复遇到。人们跑来跟我说:“C++太复杂了,你得把它简化。”然后紧接着又是:“我需要这两个功能,昨天就要,你在简化的同时,必须给我加上这两个功能。”末了再加一句:“反正你干什么都行,千万别破坏我的代码,我有一百万行呢。”这根本行不通,根本不可能。这就是为什么我现在在研究编程指南和Profiles,也就是强制性的规则集。通过这种方式,你可以设计一个Profile,确保你能用到你需要的库,同时保证你不会误用那些在你那个领域里不必要、又危险的功能。
Ryan:对于想学习C++的人,你有什么技术书籍推荐?
Bjarne:有一本我教本科生时写的书——《编程:原理与实践(使用C++)》。要学就用最新的C++,先学现代的方式,别一上来就学那些糟糕的C语言老套路。可是很多课程还是让你先学C,先学`malloc`、指针之类的问题,然后才学怎么用`vector`和`string`来避免这些问题。Profiles就是为了让编译器和静态分析器支持这种思维方式,教育者也在要求类似的东西。很多人认为Profiles只是为了处理内存安全和性能。不,它是要给人一件更好的工具,既为了学习,也为了从事特定种类的工作。
Ryan:如果你能回到职业生涯的起点,给自己一些建议,你会说什么?
Bjarne:天呐,这是个“时间机器”问题。我有时候会给我的学生布置这道题你有一台时间机器,回去给Dennis提点建议,干完这事之后,再往前跳十年,来给我一些建议。这是个很好的练习,我总能从学生那儿收到些很精彩的故事和建议。
我当时努力想避免C语言里内置类型之间的双向隐式转换,我本该在这件事上抗争得更坚决一些。我试过,但被贝尔实验室的人拦住了。他们比我更有经验,诸如此类。现在我明白了,我当时应该更进一步的。
还有,我本该推迟C++的发布,直到我能拿出类似模板的东西,这样我就能做一个更好的标准库。即便当时做出来,也不会足够好,但它能让人们从一开始就养成使用标准的习惯。当时人人都在造自己的标准库,最后是Alex Stepanov用STL救了我们,但那纯粹是运气,因为我没有推迟发布,这确实是个错误,我本应该等到能造出一个好的vector和一套类层次结构的时候。
最后,如果我能带着现在对标准委员会和臃肿官僚体制的了解回到过去,我会非常努力地去建立一个指导小组。我们会有一个五百人的社区来贡献提议,然后由一个大概只有五六个人、拥有深厚经验并且关心整门语言的小组,基于这些提案来做决策。但我当时没有那个经验和知识去提出这样的建议。
我没提工具的事。C++在工具方面一直是个短板,那是因为它成长在一个早期时代,工具匮乏,算力有限,内存有限,所以我当时也根本做不到。我给学生们布置时间机器练习时,有一条约束是:确保你给的建议在当时是有可能被采纳的。如果我只是说“我想要这个”,那么许多事情可能要到二十年之后才能做到,因此也就永远不会发生。有很多语言是奔着“为未来的计算机和程序员而设计”去的,它们中的大多数都死了,因为十年后当它们终于面世时,世界早就变了。
访谈原链接:
https://www.youtube.com/watch?v=U46fJ2bJ-co&t=40s
