开始前我要先做个澄清:这篇文章同Linus Torvalds这种死忠C程序员吐槽C++的观点是不同的。在我的整个职业生涯里我都在使用C++class="tags" href="/tags/C.html" title=c>c;而且现在C++依然是我做大多数项目时的首选编程class="tags" href="/tags/YuYan.html" title=语言>语言。自然的class="tags" href="/tags/C.html" title=c>c;当我从2007年开始做ZeroMQ(ZeroMQ项目主页)时class="tags" href="/tags/C.html" title=c>c;我选择用C++来实现。主要的原因有以下几点:
1. 包含数据结构和算法的库(STL)已经成为这个class="tags" href="/tags/YuYan.html" title=语言>语言的一部分了。如果用Cclass="tags" href="/tags/C.html" title=c>c;我将要么依赖第三方库要么不得不自己手动写一些自1970年来就早已存在的基础算法。
2. C++class="tags" href="/tags/YuYan.html" title=语言>语言本身在编码风格的一致性上起到了一些强制作用。比如class="tags" href="/tags/C.html" title=c>c;有了隐式的this指针参数class="tags" href="/tags/C.html" title=c>c;这就不允许通过各种不同的方式将指向对象的指针做转换class="tags" href="/tags/C.html" title=c>c;而那种做法在C项目中常常见到(通过各种类型转换)。同样的还有可以显式的将成员变量定义为私有的class="tags" href="/tags/C.html" title=c>c;以及许多其他的class="tags" href="/tags/YuYan.html" title=语言>语言特性。
3. 这个观点基本上是前一个的子集class="tags" href="/tags/C.html" title=c>c;但值得我在这里显式的指出:用Cclass="tags" href="/tags/YuYan.html" title=语言>语言实现虚函数机制比较复杂class="tags" href="/tags/C.html" title=c>c;而且对于每个类来说会有些许的不同class="tags" href="/tags/C.html" title=c>c;这使得对代码的理解和维护都会成为痛苦之源。
4. 最后一点是:人人都喜欢析构函数class="tags" href="/tags/C.html" title=c>c;它能在变量离开其作用域时自动得到调用。
class="tags" href="/tags/C.html" title=c>c="http://blog.jobbole.class="tags" href="/tags/C.html" title=c>com/wp-class="tags" href="/tags/C.html" title=c>content/uploads/2012/05/ZeroMQ.jpg" alt="" width="300" height="225" style="border-top-width:0px; border-right-width:0px; border-bottom-width:0px; border-left-width:0px; border-style:initial; border-class="tags" href="/tags/C.html" title=c>color:initial" />
如今class="tags" href="/tags/C.html" title=c>c;5年过去了class="tags" href="/tags/C.html" title=c>c;我想公开承认:用C++作为ZeroMQ的开发class="tags" href="/tags/YuYan.html" title=语言>语言是一个糟糕的选择class="tags" href="/tags/C.html" title=c>c;后面我将一一解释为什么我会这么认为。
首先class="tags" href="/tags/C.html" title=c>c;很重要的一点是ZeroMQ是需要长期连续不停运行的一个网络库。它应该永远不会出错class="tags" href="/tags/C.html" title=c>c;而且永远不能出现未定义的行为。因此class="tags" href="/tags/C.html" title=c>c;错误处理对于ZeroMQ来说至关重要class="tags" href="/tags/C.html" title=c>c;错误处理必须是非常明确的而且对错误应该是零容忍的。
C++的异常处理机制却无法满足这个要求。C++的异常机制对于确保程序不会失败是非常有效的——只要将主函数包装在try/class="tags" href="/tags/C.html" title=c>catclass="tags" href="/tags/C.html" title=c>ch块中class="tags" href="/tags/C.html" title=c>c;然后你就可以在一个单独的位置处理所有的错误。然而class="tags" href="/tags/C.html" title=c>c;当你的目标是确保没有未定义行为发生时class="tags" href="/tags/C.html" title=c>c;噩梦就产生了。C++中引发异常和处理异常是松耦合的class="tags" href="/tags/C.html" title=c>c;这使得在C++中避免错误是十分容易的class="tags" href="/tags/C.html" title=c>c;但却使得保证程序永远不会出现未定义行为变得基本不可能。
在Cclass="tags" href="/tags/YuYan.html" title=语言>语言中class="tags" href="/tags/C.html" title=c>c;引发错误和处理错误的部分是紧耦合的class="tags" href="/tags/C.html" title=c>c;它们在源代码中处于同一个位置。这使得我们在错误发生时能很容易理解到底发生了什么:
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">int rclass="tags" href="/tags/C.html" title=c>c = fx (); if (rclass="tags" href="/tags/C.html" title=c>c != 0) handle_error();
在C++中class="tags" href="/tags/C.html" title=c>c;你只是抛出一个异常class="tags" href="/tags/C.html" title=c>c;到底发生了什么并不能马上得知。
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">int rclass="tags" href="/tags/C.html" title=c>c = fx(); if (rclass="tags" href="/tags/C.html" title=c>c != 0) throw std::exclass="tags" href="/tags/C.html" title=c>ception();
这里的问题就在于你对于谁处理这个异常class="tags" href="/tags/C.html" title=c>c;以及在哪里处理这个异常是不得而知的。如果你把异常处理代码也放在同一个函数中class="tags" href="/tags/C.html" title=c>c;这么做或多或少还有些明智class="tags" href="/tags/C.html" title=c>c;尽管这么做会牺牲一点可读性。
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">try { … int rclass="tags" href="/tags/C.html" title=c>c = fx(); if (rclass="tags" href="/tags/C.html" title=c>c != 0) throw std::exclass="tags" href="/tags/C.html" title=c>ception(“Error!”); … class="tags" href="/tags/C.html" title=c>catclass="tags" href="/tags/C.html" title=c>ch (std::exclass="tags" href="/tags/C.html" title=c>ception &e) { handle_exclass="tags" href="/tags/C.html" title=c>ception(); }
但是class="tags" href="/tags/C.html" title=c>c;考虑一下class="tags" href="/tags/C.html" title=c>c;如果同一个函数中抛出了两个异常时会发生什么?
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">class="tags" href="/tags/C.html" title=c>class exclass="tags" href="/tags/C.html" title=c>ception1 {}; class="tags" href="/tags/C.html" title=c>class exclass="tags" href="/tags/C.html" title=c>ception2 {}; try { … if (class="tags" href="/tags/C.html" title=c>condition1) throw my_exclass="tags" href="/tags/C.html" title=c>ception1(); … if (class="tags" href="/tags/C.html" title=c>condition2) throw my_exclass="tags" href="/tags/C.html" title=c>ception2(); … } class="tags" href="/tags/C.html" title=c>catclass="tags" href="/tags/C.html" title=c>ch (my_exclass="tags" href="/tags/C.html" title=c>ception1 &e) { handle_exclass="tags" href="/tags/C.html" title=c>ception1(); } class="tags" href="/tags/C.html" title=c>catclass="tags" href="/tags/C.html" title=c>ch (my_exclass="tags" href="/tags/C.html" title=c>ception2 &e) { handle_exclass="tags" href="/tags/C.html" title=c>ception2(); }
对比一下相同的C代码:
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">… if (class="tags" href="/tags/C.html" title=c>condition1) handle_exclass="tags" href="/tags/C.html" title=c>ception1(); … if (class="tags" href="/tags/C.html" title=c>condition2) handle_exclass="tags" href="/tags/C.html" title=c>ception2(); …
C代码的可读性明显高的多class="tags" href="/tags/C.html" title=c>c;而且还有一个附加的优势——编译器会为此产生更高效的代码。这还没完呢。再考虑一下这种情况:异常并不是由所抛出异常的函数来处理。在这种情况下class="tags" href="/tags/C.html" title=c>c;异常处理可能发生在任何地方class="tags" href="/tags/C.html" title=c>c;这取决于这个函数是在哪调用的。虽然乍一看我们可以在不同的上下文中处理不同的异常class="tags" href="/tags/C.html" title=c>c;这似乎很有用class="tags" href="/tags/C.html" title=c>c;但很快就会变成一场噩梦。
当你在解决bug的时候class="tags" href="/tags/C.html" title=c>c;你会发现几乎同样的错误处理代码在许多地方都出现过。在代码中增加一个新的函数调用可能会引入新的麻烦class="tags" href="/tags/C.html" title=c>c;不同类型的异常都会涌到调用函数这里class="tags" href="/tags/C.html" title=c>c;而调用函数本身并没有适当进行的处理class="tags" href="/tags/C.html" title=c>c;这意味着什么?新的bug。
如果你依然坚持要杜绝“未定义的行为”class="tags" href="/tags/C.html" title=c>c;你不得不引入新的异常类型来区分不同的错误模式。然而class="tags" href="/tags/C.html" title=c>c;增加一个新的异常类型意味着它会涌现在各个不同的地方class="tags" href="/tags/C.html" title=c>c;那么就需要在所有这些地方都增加一些处理代码class="tags" href="/tags/C.html" title=c>c;否则你又会出现“未定义的行为”。到这里你可能会尖叫:这特么算什么异常规范哪!
好吧class="tags" href="/tags/C.html" title=c>c;问题就在于异常规范只是以一种更加系统化的方式class="tags" href="/tags/C.html" title=c>c;以按照指数规模增长的异常处理代码来处理问题的工具class="tags" href="/tags/C.html" title=c>c;它并没有解决问题本身。甚至可以说现在情况更加糟糕了class="tags" href="/tags/C.html" title=c>c;因为你不得不去写新的异常类型class="tags" href="/tags/C.html" title=c>c;新的异常处理代码class="tags" href="/tags/C.html" title=c>c;以及新的异常规范。
通过上面我描述的问题class="tags" href="/tags/C.html" title=c>c;我决定使用去掉异常处理机制的C++。这正是ZeroMQ以及Crossroads I/O今天的样子。但是class="tags" href="/tags/C.html" title=c>c;很不幸class="tags" href="/tags/C.html" title=c>c;问题到这并没有结束…
考虑一下当一个对象初始化失败的情况。构造函数没有返回值class="tags" href="/tags/C.html" title=c>c;因此出错时只能通过抛出异常来通知出现了错误。可是我已经决定不使用异常了class="tags" href="/tags/C.html" title=c>c;那么我不得不这样做:
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">class="tags" href="/tags/C.html" title=c>class foo { publiclass="tags" href="/tags/C.html" title=c>c: foo(); int init(); … };
当你创建这个类的实例时class="tags" href="/tags/C.html" title=c>c;构造函数被调用(不允许失败)class="tags" href="/tags/C.html" title=c>c;然后你显式的去调用init来初始化(init可能会失败)对象。相比于Cclass="tags" href="/tags/YuYan.html" title=语言>语言中的做法class="tags" href="/tags/C.html" title=c>c;这就显得过于复杂了。
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">struclass="tags" href="/tags/C.html" title=c>ct foo { … }; int foo_init(struclass="tags" href="/tags/C.html" title=c>ct foo *self);
但是以上的例子中class="tags" href="/tags/C.html" title=c>c;C++版本真正邪恶的地方在于:如果有程序员往构造函数中加入了一些真正的代码class="tags" href="/tags/C.html" title=c>c;而不是将构造函数留空时会发生什么?如果有人真的这么做了class="tags" href="/tags/C.html" title=c>c;那么就会出现一个新的特殊的对象状态——“半初始化状态”。这种状态是指对象已经完成了构造(构造函数调用完成class="tags" href="/tags/C.html" title=c>c;且没有失败)class="tags" href="/tags/C.html" title=c>c;但init函数还没有被调用。我们的对象需要修改(特别是析构函数)class="tags" href="/tags/C.html" title=c>c;这里应该以一种方式妥善的处理这种新的状态class="tags" href="/tags/C.html" title=c>c;这就意味着又要为每一个方法增加新的条件。
看到这里你可能会说:这就是你人为的限制使用异常处理所带来的后果啊!如果在构造函数中抛出异常class="tags" href="/tags/C.html" title=c>c;C++运行时库会负责清理适当的对象class="tags" href="/tags/C.html" title=c>c;那这里根本就没有什么“半初始化状态”了!很好class="tags" href="/tags/C.html" title=c>c;你说的很对class="tags" href="/tags/C.html" title=c>c;但这根本无关紧要。如果你使用异常class="tags" href="/tags/C.html" title=c>c;你就不得不处理所有那些与异常相关的复杂情况(我前面已经描述过了)。而这对于一个面对错误时需要非常健壮的基础组件来说并不是一个合理的选择。
此外class="tags" href="/tags/C.html" title=c>c;就算初始化不是问题class="tags" href="/tags/C.html" title=c>c;那析构的时候绝对会有问题。你不能在析构函数中抛出异常class="tags" href="/tags/C.html" title=c>c;这可不是什么人为的限制class="tags" href="/tags/C.html" title=c>c;而是如果析构函数在堆栈辗转开解(staclass="tags" href="/tags/C.html" title=c>ck unwinding)的过程中刚好抛出一个异常的话class="tags" href="/tags/C.html" title=c>c;那整个进程都会因此而崩溃。因此class="tags" href="/tags/C.html" title=c>c;如果析构过程可能失败的话class="tags" href="/tags/C.html" title=c>c;你需要两个单独的函数来搞定它:
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">class="tags" href="/tags/C.html" title=c>class foo { publiclass="tags" href="/tags/C.html" title=c>c: … int term(); ~foo(); };
现在class="tags" href="/tags/C.html" title=c>c;我们又回到了前面初始化的问题上来了:这里出现了一个新的“半终止状态”需要我们去处理class="tags" href="/tags/C.html" title=c>c;又需要为成员函数增加新的条件了…
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">class="tags" href="/tags/C.html" title=c>class foo { publiclass="tags" href="/tags/C.html" title=c>c: foo () : state (semi_initialised) { ... } int init () { if (state != semi_initialised) handle_state_error (); ... state = intitialised; } int term () { if (state != initialised) handle_state_error (); ... state = semi_terminated; } ~foo () { if (state != semi_terminated) handle_state_error (); ... } int bar () { if (state != initialised) handle_state_error (); ... } };
将上面的例子与同样的Cclass="tags" href="/tags/YuYan.html" title=语言>语言实现做下对比。Cclass="tags" href="/tags/YuYan.html" title=语言>语言版本中只有两个状态。未初始化状态:整个结构体可以包含随机的数据;以及初始化状态:此时对象完全正常class="tags" href="/tags/C.html" title=c>c;可以投入使用。因此class="tags" href="/tags/C.html" title=c>c;根本没必要在对象中加入一个状态机。
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">struclass="tags" href="/tags/C.html" title=c>ct foo { ... }; int foo_init () { ... } int foo_term () { ... } int foo_bar () { ... }
现在class="tags" href="/tags/C.html" title=c>c;考虑一下当你把继承机制再加到这趟浑水中时会发生什么。C++允许把对基类的初始化作为派生类构造函数的一部分。抛出异常时将析构掉对象已经成功初始化的那部分。
class="tags" href="/tags/C.html" title=c>ckground-class="tags" href="/tags/C.html" title=c>color:rgb(255,255,255)">class="tags" href="/tags/C.html" title=c>class foo: publiclass="tags" href="/tags/C.html" title=c>c bar { publiclass="tags" href="/tags/C.html" title=c>c: foo ():bar () {} … };
但是class="tags" href="/tags/C.html" title=c>c;一旦你引入单独的init函数class="tags" href="/tags/C.html" title=c>c;那么对象的状态数量就会增加。除了“未初始化”、“半初始化”、“初始化”、“半终止”状态外class="tags" href="/tags/C.html" title=c>c;你还会遇到这些状态的各种组合!!打个比方class="tags" href="/tags/C.html" title=c>c;你可以想象一下一个完全初始化的基类和一个半初始化状态的派生类。
这种对象根本不可能保证有确定的行为class="tags" href="/tags/C.html" title=c>c;因为有太多状态的组合了。鉴于导致这类失败的原因往往非常罕见class="tags" href="/tags/C.html" title=c>c;于是大部分相关的代码很可能未经过测试就进入了产品。
总结以上class="tags" href="/tags/C.html" title=c>c;我相信这种“定义完全的行为”(fully-defined behaviour)打破了面向对象编程的模型。这不是专门针对C++的class="tags" href="/tags/C.html" title=c>c;而是适用于任何一种带有构造函数和析构函数机制的面向对象编程class="tags" href="/tags/YuYan.html" title=语言>语言。
因此class="tags" href="/tags/C.html" title=c>c;似乎面向对象编程class="tags" href="/tags/YuYan.html" title=语言>语言更适合于当快速开发的需求比杜绝一切未定义行为要更为重要的场景中。这里并没有银弹class="tags" href="/tags/C.html" title=c>c;系统级编程将不得不依赖于Cclass="tags" href="/tags/YuYan.html" title=语言>语言。
最后顺带提一下class="tags" href="/tags/C.html" title=c>c;我已经开始将Crossroads I/O(ZeroMQ的forkclass="tags" href="/tags/C.html" title=c>c;我目前正在做的)由C++改写为C版本。代码看起来棒极了!
译注:这篇新出炉的文章引发了大量的回复class="tags" href="/tags/C.html" title=c>c;有觉得作者说的很对的class="tags" href="/tags/C.html" title=c>c;也有人认为这根本不是C++的问题class="tags" href="/tags/C.html" title=c>c;而是作者错误的使用了异常class="tags" href="/tags/C.html" title=c>c;以及设计上的失误class="tags" href="/tags/C.html" title=c>c;也有读者提到了Goclass="tags" href="/tags/YuYan.html" title=语言>语言可能是种更好的选择。好在作者也都能积极的响应回复class="tags" href="/tags/C.html" title=c>c;于是产生了不少精彩的技术讨论。建议中国的程序员们也可以看看国外的开发者们对于这种“吐槽”类文章的态度以及他们讨论问题的方式。