C++ 发展四十余年,你对 C++ 这门语言怎么看?有人觉得它太过复杂,上手难度很高;很多 C++ 开发者讨厌这门语言,他们实际上是在处理一些由那些本来就不擅长写软件的人开发的老旧产品。有人觉得如今的 C++ 已经足够现代化,而真实究竟如何?本文作者从他的视角进行了解析。
C++ 诞生至今已有四十余年,这门语言的变化与发展可谓悠久且深刻。对于 C++,不同的人有不同的看法。有些人认为它过于复杂,学习曲线陡峭,难以掌握;也有许多 C++ 开发者对这门语言心生厌倦,因为他们在日常工作中需要处理由那些不擅长编程的开发者所遗留下来的老旧代码,看都看不懂;也有人认为,如今的 C++ 已经足够现代化,具备了与时俱进的能力。那么,C++ 的真实面貌究竟如何?在本文中,作者将从自身视角出发,深入分析这门语言的现状与挑战。
以下为译文:
问大家一个问题,想请读者们诚实地回答:你上一次真正享受编程的乐趣是什么时候?
回想起来,我觉得自己大概已经有将近十年没在编程中找到乐趣了。无论是 JavaScript、Python、Ruby 还是 C,都让我提不起兴趣。当我说“乐趣”时,我指的是那种发自内心的兴奋感:你在做项目的时候会想,“天啊,这也太有意思了!我简直不敢相信我那个疯狂的点子竟然真的实现了!”
举个例子,我最近在开发一个小型的 Roguelike 游戏(随即生成地图、回合制玩法),我突然想到:“我可以用 Dijkstra 算法通过反转地图来连接房间,这样在地图生成时就能刻画出非常酷的隧道效果。” 在尝试实现这个想法时,我感到无比开心,甚至没有觉得 C++ 给我添了任何麻烦。我成功实现了,还从中学到了很多东西。接着我又想:“我能不能把 FTXUI(用 C++ 编写的终端用户界面TUI 库,https://github.com/ArthurSonzogni/FTXUI)的用户界面直接渲染到 SFML(Simple and Fast Multimedia Library,是一个用 C++ 编写的开源多媒体库)的窗口中呢?” 结果证明,我确实能做到。而且这个过程不仅没有太难,还让我深入了解了 C++ 是如何处理 Unicode 的。虽然这些事情都不简单,但它们完全可以实现,而 C++ 这门语言的应用不会阻碍我的思路。这才是真正的乐趣。
我认为,很多程序员已经忘记了当初为什么要学习编程。我学编程并不是为了帮一群亿万富翁赚更多的钱,也不是为了与一个不可变的渲染引擎斗争,只为它最终能显示一个淡蓝色的按钮,更不是为了取悦某些开源项目的掌权者。
我学编程是因为它好玩。我记得自己曾经熬夜到凌晨四点,就为了让我那蹩脚的 gwBASIC 代码能在 MS-DOS 控制台上渲染出一个字符。我记得自己为了一个奇怪的 GUI 想法或者网络服务器可以连续工作数周,只因为有个想法。我记得我一个 Bug 斗争了一个月,结果发现只是拼写错误。即便是这些挫折,回想起来也充满了乐趣,比我学到的其他任何东西都更令人兴奋。
我还记得 C++ 也曾是一门非常有趣的语言。即使在它被过度吹捧、甚至可以说是糟糕透顶的年代,C++ 仍然好玩,因为用它我几乎可以做任何事情。
然而,不知道从什么时候开始,C++ 变得“无趣”了,我觉得这种趋势几乎蔓延到了今天的每一种语言。C++ 的故事是一种警告,提醒其他语言可能走上同样的道路,同时也讲述了一段从低谷中复苏的精彩故事,只是很少有人意识到这点。
C++ 模板的“元编程末日”
在 90 年代末到 2000 年代初,我同时在使用 C++ 和 Java,直到 C++ 社区开始搞“模板元编程”这些东西时,我逐渐放弃了 C++:
这就是当时的感觉:就像一群人发现了模板元编程这个新玩具,把它当成“模板锤子”,然后拼命敲打周围一切像钉子的东西。我记得跟人讨论过一些特别离谱的事情,比如有人觉得用模板来计算两个数字的和比直接相加“更快”,虽然最后生成的汇编代码完全一模一样。
这种对模板元编程的执念,再加上 C++ 标准委员会持有一种“我们才不屑于让语言变得对程序员友好”的奇怪傲慢态度,彻底毁了我和许多人对 C++ 的兴趣。C++ 变成了那种专供“架构宇航员”用的语言,他们更在乎什么是 Pimpl 模式和模板元编程,而不是让这门语言好用一点。
这也是为什么我当时喜欢 Java 的原因。那时的 Sun Microsystems 公司真的关心程序员需要什么,Java 有垃圾回收器!它看起来非常体贴程序员,绝对不会走上同样的老路。绝对不会!Java 程序员怎么可能沉迷于疯狂的样板代码、抽象工厂模式,或者比起可用性更在乎标准化?绝对不可能,对吧?
我觉得就在那个时期,很多和我同龄的人都放弃了 C++。大家对它的印象是一门语法冗长复杂、充满尖括号的语言,而这些复杂性并没有带来多少实际收益。
但是,C++ 的发展并没有就此止步。标准委员会似乎意识到,如果他们再不采取行动,这门语言可能会沦为那种只被一个价值 10 万亿美元的行业(比如嵌入式系统)使用的冷门工具,这也太丢人了吧!于是,他们开始努力改进,彻底革新了语言,让 C++ 再次变得有趣,同时也保留了它的初衷。遗憾的是,这场巨大的革命几乎没人注意到。所以,我要来帮你更新一下对这门语言的认知。
C++11 的惊人逆袭
想要了解最新的 C++ 是什么样的,不妨去看看 C++11 新增的功能吧!那一系列令人惊叹的改进简直像是奇迹。让我来挑几个重点给你解释一下:
auto 自动类型推断:是的,C++ 居然引入了一个可以自动推断变量类型的关键词!这让我大吃一惊,因为我还记得以前的 C++ 简直像个死板的老顽固,强迫你一个词写好几遍,完全没必要。
nullptr:他们终于修复了 C 语言中那个尴尬的 NULL,它既是数字 0 又不是数字 0,只是因为某个标准委员会成员还在为古董级的 PDP-11 计算机写代码。
range-for 循环:没错,C++ 引入了类似 Python 的简化迭代器。而且它还能配合 auto 使用,你可以轻松地循环遍历列表,完全不用纠结类型问题。
Lambda 表达式:是的,C++ 居然有了 Lambda 表达式了!而且它比 Python 的 Lambda 强大太多了。这些 Lambda 大大改变了编程的设计方式,比如 FTXUI 就用它们来显著提升 API 的可用性。
<chrono> 时间库:你没看错,2011 年的 C++ 居然有了比大多数语言都要强大的时间库。你甚至可以用它创建一个“100 毫秒”的时间点,语法简洁又安全。
<regex> 正则表达式:是的,C++ 内置了正则表达式,而且效果非常好。虽然它的“ECMAScript 模式”跟 JavaScript 的正则略有差异,但在大多数情况下用起来都非常顺畅。
unique_ptr 和 shared_ptr 智能指针:它们提供了引用计数和内存所有权管理功能。从这些特性引入后,人们几乎不再频繁使用堆分配,而是更倾向于在栈上存储数据,仅在必要时用这些智能指针。
<thread> 多线程支持:是的,多线程现在已经是语言的默认功能了。以前的 C++ 像是在说:“你想用线程?那你自己写吧,笨蛋。” 而现在的 C++ 却在说:“线程很棒啊!拿去用吧!”
老实说,我不知道其中发生了什么,为什么 C++11 会让这门语言焕然一新,但它让我想起 JavaScript 的 ES6。就像是一场彻底的哲学和风格大转变,同时又没有抛弃过去的用法。
是的,C++有这些功能……
更重要的是,现在的 C++ 已经是一门现代化语言了,很多你在其他语言中习惯的功能它都有。以下是一些常见需求和对应的解决方案:
需要遍历文件系统?看看 filesystem 模块。
需要包管理工具?可以试试 Conan、Meson’s WrapDB 和 vcpkg。
需要矩阵库?试试 Eigen。
关于深度学习?顺便提一句,TensorFlow 是用 C++ 写的。
图形处理?你知道很多游戏都是用 C++ 写的吧?推荐从 SFML 入门,体验不错。
图形用户界面(GUI)? 你可以用 Qt 或 wxWidgets,或者试试 ImGui。
终端用户界面(TUI)?是的,我相信 FTXUI 是任何语言里设计最好的 GUI 库(针对终端界面)。它真的很棒,但仅限于终端 UI。
总的来说,C++ 基本能满足你的所有需求。但老实说,它的生态质量有好有坏。一般来说,质量比你预期的要高,而且比我在 JavaScript 和 Python 里见到的高。我觉得说“生态质量参差不齐”其实可以适用于任何语言。
如果我们都实话实说的话,你真的觉得 Python 的包管理工具非常顶级吗?你真的觉得有 10 种包管理器很正常吗?再想想 GUI 的 API 质量?文件系统 API 的设计?说实话,这些都不算好用,C++ 的水平其实和其他语言差不多,甚至大部分还稍微好一些。像 FTXUI 这样的库简直让人惊艳,STL(标准模板库)也相当棒。
好吧,但你不是说“有趣”吗?
我似乎听到很多读者在反驳了:“哦对哦,那 Rust 有很棒的 Web 服务器……还有数据结构……还有……还有标准库。Rust 和 C++ 一样有趣!”
是的,我的重点并不是说这些新功能让 C++ 变得有趣。重点是:
如果你还觉得 C++ 是那种充满尖括号、指针和各种令人抓狂的东西的古老语言,那你就错了。
我发现 C++ 现在可能是现存功能最强大的语言,而且优势非常明显。我还没遇到过它做不到的事情,不仅如此,对于我想做的几乎所有事情,它都有多个选择:
想做桌面应用程序?你可以手写代码,用像 Fenster 这样的工具,或者用 SFML,或者试试 Qt,也可以直接按照老派方法写代码(Fenster 提供了很好的教程)。
想播放声音?有各种开源的、收费的、甚至是 DIY 的方案。你可以用 SFML 快速搞定,也可以付费使用像 Wwise 这样的行业顶级工具。
想做 3D 图形?选择多到让人眼花缭乱:OpenGL、Vulkan、Direct3D、Ogre3D……你出门随便一走就会撞到一个 3D 库。
需要数学库?从 BLAS、Eigen 到专为游戏设计的 GMTL,总能找到适合你的工具。
想做 AI?知道吗?TensorFlow 实际上就是用 C++ 写的。
没有现成的库怎么办?直接调用几乎任何操作系统的 API 自己实现也不难,而自己动手写工具的过程本身就很有趣。
这些只是几个例子,但最重要的一点是:C++ 不会妨碍你做事。你几乎可以直接访问任何用 C 或 C++ ABI 写的库,也可以直接操作每个操作系统的接口,完全可以自己实现你需要的任何东西。
C++ 不会过于在意流行趋势、固定的编程风格
没错,用 C++,你几乎可以做任何事。而且,自 2011 年以来,C++ 标准委员会已经调整了方向,更加注重开发者的生产力。
那么,为什么这很有趣呢?
C++ 现在处于一个奇妙的发展点上:语言和生态系统质量非常高,但它又不够流行,所以吸引不到那些会毁掉语言的怪咖们。
你知道我在说谁吧?还记得我之前提到我离开 C++ 是因为一群怪人疯狂痴迷于设计模式和模板元编程?后来我开玩笑说,Java 因为设计模式和那些糟糕的 XML 文件遭遇了同样的问题?接着是 Ruby,尤其是 Ruby on Rails 火起来的时候?然后是 Python、JavaScript、Rust……几乎每次一门语言变得流行,总会有那些让人讨厌的家伙跳出来把语言搞砸。
而现在,C++ 已经不流行了,所以这些人不在乎它了。C++ 甚至不流行到连白宫都讨厌它,那些到处拿着标准文件阻碍语言发展的激进者们,早已跑去其他语言搞他们的网红式生意了。
这些家伙正在你的社交媒体下留言,喊着:“为啥不用 Rust?!”或者大吼:“你居然不用 React?!”还有人大声嚷嚷:“你 Rust 代码里为啥这么多 unsafe?!”又或者嘲讽:“天哪!Ruby 代码里居然用 for 循环!真是外行!” 幸运的是,C++ 不够流行,没法让这些骗子获取到什么,所以他们基本无视它。
当然,Rust 的某些狂热粉可能会来烦你,但他们通常忙着跟借用检查器(borrow checker)较劲,根本没时间干别的事,所以你可以忽略他们。
这意味着 C++ 和它的社区完全不在乎你干什么。
想写个矩阵库?尽管去做。
想开发个 GUI 库?放手去干。
想写个游戏引擎?关灯躺沙发上,自己慢慢搞吧。
甚至想做个看你写代码然后嘲笑你的奇怪小游戏?真的没人会管,大多数人还会觉得这很有趣或者很酷。
“完全没人在意”是创意的关键
“但是,有开发者会有疑问,难道我们不需要一个伟大光辉的领袖来指导我们行为吗?C++ 缺少那种专制的‘仁慈终身独裁者’,难道不会导致没有方向感吗?难道我们不会犯错吗?”
你可能不会直接这么说,但你的潜台词可能是这样的。这种心态正是为什么其他语言不那么有趣的原因之一。其他语言通常由某些组织严格控制,而这些组织会对你尝试做酷东西的时候踩上一脚。如果你想做一个与某个“仁慈独裁者”获利项目竞争的东西,那你最好准备好被他们的“打手”追着骂上几年。
我坚信,要让创造力真正绽放,你需要在没有恐惧、没有批评的环境下,把自己的想法释放出来。你得能自由地表达自己,等到你拼尽全力将这个想法从脑海中拽出来之后,才有资格回头批判它。当然,一旦你的作品发布出去,就得做好心理准备接受批评。不过,从“自由创作”到“专注提升质量”的这种模式转换,是伟大创作的关键。
我学到这一点的最好例子来自以前的一位画画老师——其实,每个画画老师都差不多。他们会走到学生的未完成作品旁边,试图纠正错误。烦人的是,他们通常站在错误的地方(比如他们 5 英尺高的视线和我 6 英尺 2 英寸的视线完全不同),说我的画不对;或者他们会评论某些正在进行的部分,而这些部分根本没办法脱离整个画面上下文来评判。
这些老师的错误在于,他们认为早期的“错误”无法被后续改正,而会一直存在于最终作品中。但事实上,学生最终创作出的作品可能完全没有问题,而在创作过程中,很多东西看起来不对劲纯粹是因为“未完成”。
这是关于创作的重要事实:在“丑陋的中间阶段”,一切看起来都很糟糕。
一开始可能会觉得很棒,但随着工作的深入,事情会变得混乱,因为你需要不断调整和重塑一切,才能实现最终的愿景。这个过程更像是走钢丝,而不是开车——你在细线上保持平衡,试图到达另一端,而唯一学会走钢丝的方法就是反复摔倒,直到找到正确的平衡。
很多其他语言的环境就像是中途有人试图把你从钢丝上推下去,然后卖给你一辆车。
C++ 的几乎无限创作空间,加上相对少的干扰,让它成为一种极具创意的体验。如果我有一个想法,我唯一的限制是我的技能,而不是语言的局限性,也不是某个 C++ 网红大佬的指指点点。
cppreference.com 是神器
我认为 cppreference.com(https://cppreference.com/)是我用过的最好的编程语言文档网站。它几乎具备了我对一门语言文档的所有期待:
每个关键字和库都附有详尽的参考资料。
几乎每个功能都有全面的示例代码,并且绝大多数都能正常运行。我只遇到过大约 5% 的示例代码不能直接运行,而这通常是由于编译器的实现问题,而不是文档本身的错误。
所有功能都清晰标明了引入的 C++ 标准版本,并附有链接,指向相关标准文档和其他参考资料。
搜索栏支持 DuckDuckGo,所以即使你拼错单词或者记不住确切术语,依然能找到需要的内容。
除了参考资料和示例代码,它还提供对重要概念的完整解释,并配有示例。比如关于 Copy Elision(拷贝省略) 的文档,虽然技术性很强,但写得很好,包含了许多示例,帮助理解。
唯一缺失的地方是如何在不同平台上安装编译器。我对这一点持矛盾态度。对于其他语言来说,这种安装指南可能很重要,但对 C++ 来说,我觉得他们很可能会因为厂商(例如微软)的阻力而无法提供。然而,考虑到 C++ 的编译器和平台其实不多,提供一系列的入门指南应该是可行的。
并非一切都是完美的
尽管我非常享受用 C++ 编程,但我绝不是在说 C++ 是一门毫无缺点的语言。我的观点是,C++ 现在已经和其他语言一样好,而没有语言是完美的。无论用哪种语言,你都会遇到让人抓狂的问题,只不过 C++ 的问题和其他语言不同。
比如,在 Windows 上使用 C++ 时,要安装非微软的编译器和开发工具非常麻烦。MSYS2(一个用于 Windows 的工具集)在这里起不到什么作用,所以别提了。我不得不写一整套 PowerShell 脚本,只为简化编译器和开发工具的安装。如果你觉得这很糟,那你怎么还能忍受 Python?在 Windows 上安装 Python 时,安装程序通常不会把 Python 添加到 PATH,除非是微软或 ActiveState 的“官方安装程序”,才会被允许添加到 PATH。
C++ 在这方面其实略胜一筹,因为你不需要面对一些“潜规则”或者“后台交易”。比如微软和 ActiveState 的安装程序可以绕过规定,直接修改环境变量 PATH,而其他程序就不允许这么做。这种情况本身就让人很烦,C++ 的好处就是避免了与那些觉得这种事情“正常”的人争论。
如果你觉得自己的编程语言更好,给我一个小时,我能找到足够多让你抓狂的问题,让你觉得你的语言就像个一团糟的噩梦。所有语言都有问题,C++ 的问题并不会毁了它的乐趣。
以下是我目前使用 C++ 时遇到的一些痛点问题:
C++ 编译器错误提示算是行业里面最糟糕的,说这话我并不是夸张。如果 C++ 标准委员会想要产生最大的影响力,他们应该标准化错误信息格式,因为目前的错误信息几乎是反人类的。
构建工具非常糟糕。 我不知道为什么 C++ 的构建工具如此难用,开发这些工具的人总是做出最愚蠢的决定。目前我发现最好的工具是 Meson,但它也有一堆反人类的设计,比如无视已安装的包指令,而是使用有问题的系统库且完全不通知你。我就在写这篇文章当天遇到了这个问题,真是气得想砸键盘。
编译器开发者没有动力遵循标准。我提到过 Clang 的一个 bug,这就是你偶尔会碰上的问题之一。我真心觉得标准委员会应该专门开发一个页面,列出编译器的标准兼容性,以及那些明显存在问题却迟迟不修复的编译器。
由于历史包袱过重,这门语言确实非常复杂。C++ 里有很多遗留的东西,入门者需要了解这些复杂的部分才能找到真正有用的内容。我的理解是,C++ 就像是三个(甚至更多)不同风格的语言,有点像 JavaScript。首先是旧的风格,几乎所有东西都在堆上分配内存,原始指针到处都是;接着是模板元编程风格,像一场模板灾难;然后是 2011 年以后的一种新风格,这时几乎所有东西都在栈上分配,指针少得可怜;最近在 C++ 中又出现了一种新的编程风格,这种风格更倾向于使用 struct 而不是 class,同时也避免了使用太多的继承(即类之间的层次关系)。要想掌握这些不同“时代”的 C++ 风格确实很难,而且很多代码审查的视频博主似乎都卡在了某一个特定 C++ 风格时代。
我讨厌 RAII(资源获取即初始化)。我发现 RAII 在许多真实场景中反而是种阻碍,尤其是在复杂配置中。这个“特性”更像是语言的一个漏洞,而非真正有用的功能。我的看法是,C++ 可以在现有的 RAII 和其他语言的初始化构造函数之间找到一种折中方案。
总之,我只是想表达我用 C++ 做一些“蠢事”时的乐趣,同时科普一下,C++ 并不是你以为的那种语言。如果你碰到某个课程试图用大量尖括号来贬低 C++,只需要知道,这种课程的作者其实完全不懂自己在说什么。比如,看看这段用 FXTUI 写的 GUI 布局代码:
document_ = Renderer([&]{
return hbox({
hflow(
vbox(
text(format("HP: {}", player_.hp)) | border,
text(status_text_) | border ) | xflex_grow ),
separator(),
hbox(map_view_->Render()),
});
});
老实说,如果你不知道这是 C++,你能猜到它是什么语言吗?讲真,猜不到吧。