为什么 C++
本文转载自 刘未鹏(pongba) 在 2007 年和 Bjarne 沟通的文章,由 waterwalk 翻译。
问题
为什么用 C++ 呢?在你皱着眉头离开之前,试着回答这个简单的问题。效率,是么?人人都知道这个。但情况是,当一个人开始讨论编程语言或与其相关的话题时,他必须要非常明确而有针对性。为什么呢?我来问你另一个问题:如果效率是人们使用 C++ 的唯一理由,那么为啥不直接用 C 呢?C 被认为比 C++ 效率更高(嗯嗯,我知道 C 没有比 C++ 的效率高多少,所以这里别误解我的意思,因为即使它们二者效率相同,刚才的问题依然存在)。
迷思
我知道你又要说 “更好的抽象机制” 了,因为毕竟 C++ 是要设计成一个更好的 C 的。C++ 没有牺牲效率,同时又添加了这么多高级特性。但问题是,“开发者们真的需要这些高级特性么?”。毕竟我们一直听人讲 KISS 之类的东西。我们也都听到有声称 C 比 C++ 更 KISS 所以我们要用 C 云云。这种持续不断的争论将 C 与 C++ 之间的比较变成了一个大大的迷题(或者说是混乱)。令人惊讶的是,貌似的确有很多人更加倾向于用 C,最大的理由就是 C++ 实在是太难用对了。甚至 Linus 也这么想。
这种现象最大的影响就是当人们在 C 和 C++ 之间权衡时,使人们倾向于使用 C。而且一旦人们开始用 C,他们很快就适应并满足了(其实,在任何语言乃至任何人类活动中都有此现象,C++ 亦然,比如常常听到有人说 “XX 语言我用了这么多年,一直用得好好的”,照这种说法任何图灵完备的语言还不都是能用来编程?)。于是即使他们还没有试试 C++,或者他们还没成为好的 C++ 程序员时,他们就开始声称 C 比 C++ 更好了。然而其实呢,真实的答案往往总是取决于实际情况的。
我说过 “取决于实际情况” 了么?那到底实际情况是什么呢?显然,有些领域 C 是更好的选择。例如设备驱动开发就不需要那些 OOP/GP 技巧。而只是简单的处理数据,真正重要的是程序员确切地知道系统是如何运转的,以及他们正在做什么。那么写操作系统呢?我本人并没有参与任何操作系统的开发,但我读过不少操作系统代码(大多是 unix 的)。我的感觉是操作系统很大一部分也不需要 OOP/GP。
但是,这就表示在所有效率重要的领域,C 都是比 C++ 更好的选择么?未必。
答案
让我们一个一个来分析。
首先,当人们关注效率时,有 2 种效率 —— 时间效率(例如 OS,运行时库,实时应用程序,high-demanding 的系统)和空间效率(例如各种嵌入式系统)。但是,这样的分类并不能帮我们决定用 C 还是 C++,因为 C 和 C++ 的时空效率都很高。真正影响选择语言的因素是业务逻辑(这里的 “业务逻辑” 并非表示 “企业应用业务”)。例如,使用 OOP/GP 来表达逻辑(或者说代码的结构)好呢,还是就只用数据和过程好呢?
据此观点,我们可以把应用程序大致分为两类(当然前提是关注的是 C/C++ 而不是 java/C#/ruby/erlang 等等):底层应用程序和高层应用程序。这里底层是指像 OB/OO 和 GP 没啥用处的地方, 其余归到高层。显然,在所有 C/C++ 应用的领域(这些领域需要 C/C++ 的效率),属于高层的应用有很多(可以看看 Bjarne Stroustrup 在他主页上的列表)。在这些领域中,抽象至少是和效率一样重要的。而这些正是 C++ 适用的场合。
等等还有。即使在程序员不需要高级抽象的领域,也不是就绝对用不到 C++ 的。为啥呢?仅仅是因为你的代码中没有用类或模板并不意味着不能用以类或模板实现的库。因为有如此众多方便的 C++ 库(还有即将到来的 tr1/tr2),我觉得有充分的理由在这些领域中使用 C++—— 你可以在编码时仅使用 C++ 中的 C 核心(以任何你喜欢的方式来 KISS),同时还能用强大的 C++ 库(比如 STL 容器、算法和 tr1/tr2 的组件)。
最后,我认为人们还常常忽略了一点 —— 有时 KISS 也是建立在抽象上的。我觉得 Matthew Wilson 在他新书《Extended STL,卷 1》的序言中对此做了很好的阐释。他写了 2 段代码,一段用 C,另一段用 C++:
//in c
DIR *dir = opendir(".");
if (NULL != dir)
{
struct dirent *de;
for (; NULL != (de = readdir(dir));)
{
struct stat st;
if (0 == stat(de->d_name, &st) && S_IFREG == (st.st_mode & S_IFMT))
{
remove(de->d_name);
}
closedir(dir);
}
//in C++
readdir_sequence entries(".", readdir_sequence::files);
std::for_each(entries.begin(), entries.end(), ::remove);
而在 C++ 0x 里面更简单:
std::for_each(readdir_sequence(".", readdir_sequence::files), ::remove);
也就是说,我认为即使一个人在自己的代码里不需要类或模版,他也有理由用 C++,因为他用的那些方便的 C++ 库用到了类和模板。如果一个高效的容器(或智能指针)能把你从无聊的手动内存管理中解放出来,为啥还要用那原始的 malloc/free 呢?如果一个更好的 string 类(我可没说 std::string,地球人都知道那个不是 C++ 中能做出的最好的 string 类)或正则表达式类能把你从一坨一坨的、你看都不想看的处理字符串的代码中解脱出来,那么为啥还要手动去做这些事呢?如果一个 “transform”(或 “for_each”)能够用一行代码把事情漂亮搞定,为啥还要手写一个 for 循环呢?如果高阶函数能满足你的需要,那么为啥还要用笨拙的替代方法呢?(OK,我知道,最后两个需要 C++ 加入 lambda 支持才真正摆脱鸡肋的骂名 —— 这正是 C++0x 的任务嘛)
总之,我认为 KISS 并不等同于 “原始”;KISS 意味着用最适合的工具来做事情,这里 “最合适” 的意思是工具能够帮你以尽量直接简洁的方式来表达思想,同时又不降低代码的可读性,另外还保持代码容易理解。
真正的问题
人们可能会说,相较于被正确使用而言,C++(远远)更容易被错误使用。而相比而言,C 程序的复杂性更容易管理和控制。在 C++ 中,一个普通程序员很可能会写出一堆高度耦合的类,很快情况就变得一团糟。但这个其实是另外一个问题。在另一方面,这种事情也很可能发生在任何一门面向对象语言中,因为总是有程序员在还没弄懂什么是 HAS-A 和 IS-A 之前,就敢于在类上再写类,叠床架屋的一层一层摞上去。他们学会了在一门特定的语言中如何定义类,如何继承类的语法,然后他们就认为自己已经掌握了 OOP 的精髓了。另一方面,这一问题在 C++ 中更为严重,因为 C++ 有如此众多的偶然复杂性在阻碍设计;而且 C++ 又是如此灵活,很多问题在 C++ 中都有好几种解决办法(想想那么多的 GUI 库吧),于是在这些选择中进行权衡本身就成了一个困难。C++ 中的非本质复杂性是其历史包袱使然,而 C++0x 正是要努力消除这些非本质复杂性(在这方面 C++0x 的工作的确做得很不错)。对于设计来说,灵活性不是个坏事情 —— 可以帮助好的设计者作出好的设计。如果有人抱怨说这个太费脑细胞了,那可能是这个设计者本身的问题,而不能怪语言。可能就不该让他来作设计。如果你担心 C++ 的高级特性会把你的同事引入歧途,把项目搞砸,那你也许应该制定一份编码标准并严格推行(或者你也可以遵循 C++ 社群这些年积攒下来的智慧,或者在必要时,只使用 C++ 中的 C 或 C with class 那部分),而不是因为有风险就躲开 C++(其实这些风险可以通过一些政策来避免的),因为那样的话,你就没法用那些 C++ 的库了。
另一方面,其实一个更为重要的问题是一个心理学问题 —— 如果一门语言中存在某个奇异的特性或旮旯,那么迟早总会有人发现的,总会有人为之吸引的,然后就使人们从真正有用的事情中分心出来(这有点像 Murphy 法则),更不用说那些有可能对真正问题带来(在某种程度上)漂亮的解决方案的语言旮旯了。人们本性上就容易受到稀有资源的诱惑。奇技淫巧是稀有资源,于是奇技淫巧便容易吸引人们的注意力,更别说掌握一个技巧还能够让那人在他那圈子里感觉非常牛了。退一万步,你会发现,即使是一个废柴技巧也能引起人们足够的兴趣来。
C++ 中有多少阴暗角落呢?C++ 中又有多少技巧呢?总的来说,C++ 中,有多少非本质复杂性呢?(懂一定 C++ 的人一定知道我在说什么)
平心而论,近年来(现代 C++ 中)发现的大多数技巧或(如果你愿意称之为)技术实际上都是由实际需求驱动的,尤其是需要实现高度灵活而又普遍适用(generic)的类库 (例如 boost 中的那些玩意)。而这些技巧也的确(在某种程度上)提供了对实际问题的漂亮解决方案。让我们来这么想一下,如果你处于一个两难境地:要么用那些奇技淫巧来做点很有用的东西,要么不做这样其他人也就没得用。你会如何选择呢?我知道 boost 的英雄们选择了前者 —— 不管多么困难多么变态多么龌龊,把它做出来!
但所有这些争论都不能改变一个事实:我们理应享有一个语言,能够让我们用代码清晰的表达思想。以 boost.function/boost.bind/boost.tuple 为例,variadic templates 可以大大简化这几个库的实现(减至几乎是原先 1/10 的代码行数),同时代码也(远远)更加简洁易懂。Auto,initializer-list,rvalue-reference,template-aliasing,strong-typed enums,delegating-constructors,constexpr,alignments,inheriting-constructors,等等等等,所有这些 C++0x 的特性,都有一个共同目的 —— 消除语言中多方面的非本质复杂性或语言中的尴尬之处。
正如 Bjarne Stroustrup 所说,很显然 C++ 太过复杂了,很显然人们被吓坏了,并且时不时就不用 C++ 了。但 “人们需要相对复杂的语言去解决绝对复杂的问 题”。我们不能通过减少语言特性而使其更加强大。复杂的特性就连模板甚至多继承这样的也是有用的 —— 如果你正好需要它们,而且如果你极其小心使用,不要搬起石头砸自己的脚的话。其实在所有 C++ 的复杂性当中,真正阻碍了我们的是 “非本质复杂性”(有人称之为 “尴尬之处”),而不是语言所支持的编程范式(其实也就 3 个而已)。而这也正是我们应该拥抱 C++0x 的重要原因,因为 C++0x 正是要消除那些长期存在的非本质复杂性,同时也使得那些奇技淫巧不再必要(很显然,目前这些技巧堆积如山,翻翻那些个 C++ 的书籍,或者瞅瞅 boost 库,你就知道我在说啥了),这样我们就能够直观清晰的表达思想。
结论
C++ 难用,更难用对。所以当你决定用它时,要小心,要时刻牢记自己的需求,所要达到的目的。这里有一个简单的指南:
我们需要高效率么?如果需要,那么我们需要抽象么(请仔细思考这一点,因为很难评估使用 C++ 高级特性是否能够抵消误用这些机制的风险;正确的回答取决于程序员的水平有多高,遵循哪种编码标准以及编码标准执行得如何,等等)?
如果是,那么用 C++ 吧。如果不是,那么,我们需要用 C++ 库来简化开发么?
如果是,那就用 C++ 吧。但同时必须时刻牢记你在做什么 —— 如果你的代码不需要那些 “漂亮的” 抽象,那就别试图使用以免陷入其中。别只是因为你在.cpp 文件中写代码以及你用的是 C++ 编译器就要用类啊、模板啊这些东西。
如果不是,那就用 C,不过你又会想为啥不仅仅使用 C++ 中属于 C 的那部分核心呢?还是老原因:人们很容易就陷入到语言的 “漂亮” 特性中去了,即使他们还不知道这些特性是否有用。我都记不清有多少次自己写了一大堆的类和继承,到最后反倒要问自己 “要这么些个类和继承做什么呀?”。所以,如果你能坚持只用 C++ 中 C 或 C with class 的那部分,并遵循 “让简单的事情保持简单” 的理念;或者你需要把 C 代码迁移到 C++ 中来的话,那么就用 C++ 吧,但要十分小心。另一方面,如果你既不需要抽象机制,也不需要 C++ 库,因为事情非常简单,不需要方便的组件例如容器和字符串,或者你已认定 C++ 能够给项目带来的好处微乎其微,不值得为之冒风险,或者干脆就没那么多人能用好 C++,那么可能你还是只用 C 的好。
底线是:让简单的事情保持简单(但同时也请记住:简单性可以通过使用高级库来获得);必要时才使用抽象(切记不可滥用;遵循好的设计方法和最佳实践)。