来源: http://blog.jobbole.com/19647/

开始前我要先做个澄清:这篇文章同Linus Torvalds这种死忠C程序员吐槽C++的观点是不同的。在我的整个职业生涯里我都在使用C++,而且现在C++依然是我做大多数项目时的首选编程语言。自然的,当我从2007年开始做ZeroMQ(ZeroMQ项目主页)时,我选择用C++来实现。主要的原因有以下几点:

1.  包含数据结构和算法的库(STL)已经成为这个语言的一部分了。如果用C,我将要么依赖第三方库要么不得不自己手动写一些自1970年来就早已存在的基础算法。

2.  C++语言本身在编码风格的一致性上起到了一些强制作用。比如,有了隐式的this指针参数,这就不允许通过各种不同的方式将指向对象的指针做转换,而那种做法在C项目中常常见到(通过各种类型转换)。同样的还有可以显式的将成员变量定义为私有的,以及许多其他的语言特性。

3.  这个观点基本上是前一个的子集,但值得我在这里显式的指出:用C语言实现虚函数机制比较复杂,而且对于每个类来说会有些许的不同,这使得对代码的理解和维护都会成为痛苦之源。

4.  最后一点是:人人都喜欢析构函数,它能在变量离开其作用域时自动得到调用。

为什么我希望用C而不是C++来实现ZeroMQ

如今,5年过去了,我想公开承认:用C++作为ZeroMQ的开发语言是一个糟糕的选择,后面我将一一解释为什么我会这么认为。

首先,很重要的一点是ZeroMQ是需要长期连续不停运行的一个网络库。它应该永远不会出错,而且永远不能出现未定义的行为。因此,错误处理对于ZeroMQ来说至关重要,错误处理必须是非常明确的而且对错误应该是零容忍的。

C++的异常处理机制却无法满足这个要求。C++的异常机制对于确保程序不会失败是非常有效的——只要将主函数包装在try/catch块中,然后你就可以在一个单独的位置处理所有的错误。然而,当你的目标是确保没有未定义行为发生时,噩梦就产生了。C++中引发异常和处理异常是松耦合的,这使得在C++中避免错误是十分容易的,但却使得保证程序永远不会出现未定义行为变得基本不可能。

在C语言中,引发错误和处理错误的部分是紧耦合的,它们在源代码中处于同一个位置。这使得我们在错误发生时能很容易理解到底发生了什么:

int rc = fx ();
if (rc != )
handle_error();

在C++中,你只是抛出一个异常,到底发生了什么并不能马上得知。

C++
int rc = fx();
if (rc != )
throw std::exception();

这里的问题就在于你对于谁处理这个异常,以及在哪里处理这个异常是不得而知的。如果你把异常处理代码也放在同一个函数中,这么做或多或少还有些明智,尽管这么做会牺牲一点可读性。

C++
try {

int rc = fx();
if (rc != )
throw std::exception(“Error!”);

catch (std::exception &e) {
handle_exception();
}

但是,考虑一下,如果同一个函数中抛出了两个异常时会发生什么?

C++
class exception1 {};
class exception2 {};
try {

if (condition1)
throw my_exception1();

if (condition2)
throw my_exception2();

}
catch (my_exception1 &e) {
handle_exception1();
}
catch (my_exception2 &e) {
handle_exception2();
}

对比一下相同的C代码:

C

if (condition1)
handle_exception1();

if (condition2)
handle_exception2();

C代码的可读性明显高的多,而且还有一个附加的优势——编译器会为此产生更高效的代码。这还没完呢。再考虑一下这种情况:异常并不是由所抛出异常的函数来处理。在这种情况下,异常处理可能发生在任何地方,这取决于这个函数是在哪调用的。虽然乍一看我们可以在不同的上下文中处理不同的异常,这似乎很有用,但很快就会变成一场噩梦。

当你在解决bug的时候,你会发现几乎同样的错误处理代码在许多地方都出现过。在代码中增加一个新的函数调用可能会引入新的麻烦,不同类型的异常都会涌到调用函数这里,而调用函数本身并没有适当进行的处理,这意味着什么?新的bug。

如果你依然坚持要杜绝“未定义的行为”,你不得不引入新的异常类型来区分不同的错误模式。然而,增加一个新的异常类型意味着它会涌现在各个不同的地方,那么就需要在所有这些地方都增加一些处理代码,否则你又会出现“未定义的行为”。到这里你可能会尖叫:这特么算什么异常规范哪!

好吧,问题就在于异常规范只是以一种更加系统化的方式,以按照指数规模增长的异常处理代码来处理问题的工具,它并没有解决问题本身。甚至可以说现在情况更加糟糕了,因为你不得不去写新的异常类型,新的异常处理代码,以及新的异常规范。

通过上面我描述的问题,我决定使用去掉异常处理机制的C++。这正是ZeroMQ以及Crossroads I/O今天的样子。但是,很不幸,问题到这并没有结束…

考虑一下当一个对象初始化失败的情况。构造函数没有返回值,因此出错时只能通过抛出异常来通知出现了错误。可是我已经决定不使用异常了,那么我不得不这样做:

C++
class foo
{
public:
foo();
int init();

};

当你创建这个类的实例时,构造函数被调用(不允许失败),然后你显式的去调用init来初始化(init可能会失败)对象。相比于C语言中的做法,这就显得过于复杂了。

C
struct foo
{

};
int foo_init(struct foo *self);

但是以上的例子中,C++版本真正邪恶的地方在于:如果有程序员往构造函数中加入了一些真正的代码,而不是将构造函数留空时会发生什么?如果有人真的这么做了,那么就会出现一个新的特殊的对象状态——“半初始化状态”。这种状态是指对象已经完成了构造(构造函数调用完成,且没有失败),但init函数还没有被调用。我们的对象需要修改(特别是析构函数),这里应该以一种方式妥善的处理这种新的状态,这就意味着又要为每一个方法增加新的条件。

看到这里你可能会说:这就是你人为的限制使用异常处理所带来的后果啊!如果在构造函数中抛出异常,C++运行时库会负责清理适当的对象,那这里根本就没有什么“半初始化状态”了!很好,你说的很对,但这根本无关紧要。如果你使用异常,你就不得不处理所有那些与异常相关的复杂情况(我前面已经描述过了)。而这对于一个面对错误时需要非常健壮的基础组件来说并不是一个合理的选择。

此外,就算初始化不是问题,那析构的时候绝对会有问题。你不能在析构函数中抛出异常,这可不是什么人为的限制,而是如果析构函数在堆栈辗转开解(stack unwinding)的过程中刚好抛出一个异常的话,那整个进程都会因此而崩溃。因此,如果析构过程可能失败的话,你需要两个单独的函数来搞定它:

C++
class foo
{
public:

int term();
~foo();
};

现在,我们又回到了前面初始化的问题上来了:这里出现了一个新的“半终止状态”需要我们去处理,又需要为成员函数增加新的条件了…

C++
class foo
{
public:
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 ();
...
}
};

将上面的例子与同样的C语言实现做下对比。C语言版本中只有两个状态。未初始化状态:整个结构体可以包含随机的数据;以及初始化状态:此时对象完全正常,可以投入使用。因此,根本没必要在对象中加入一个状态机。

C
struct foo
{
...
}; int foo_init ()
{
...
} int foo_term ()
{
...
} int foo_bar ()
{
...
}

现在,考虑一下当你把继承机制再加到这趟浑水中时会发生什么。C++允许把对基类的初始化作为派生类构造函数的一部分。抛出异常时将析构掉对象已经成功初始化的那部分。

C++
class foo: public bar
{
public:
foo ():bar () {}

};

但是,一旦你引入单独的init函数,那么对象的状态数量就会增加。除了“未初始化”、“半初始化”、“初始化”、“半终止”状态外,你还会遇到这些状态的各种组合!!打个比方,你可以想象一下一个完全初始化的基类和一个半初始化状态的派生类。

这种对象根本不可能保证有确定的行为,因为有太多状态的组合了。鉴于导致这类失败的原因往往非常罕见,于是大部分相关的代码很可能未经过测试就进入了产品。

总结以上,我相信这种“定义完全的行为”(fully-defined behaviour)打破了面向对象编程的模型。这不是专门针对C++的,而是适用于任何一种带有构造函数和析构函数机制的面向对象编程语言。

因此,似乎面向对象编程语言更适合于当快速开发的需求比杜绝一切未定义行为要更为重要的场景中。这里并没有银弹,系统级编程将不得不依赖于C语言。

最后顺带提一下,我已经开始将Crossroads I/O(ZeroMQ的fork,我目前正在做的)由C++改写为C版本。代码看起来棒极了!

译注:这篇新出炉的文章引发了大量的回复,有觉得作者说的很对的,也有人认为这根本不是C++的问题,而是作者错误的使用了异常,以及设计上的失误,也有读者提到了Go语言可能是种更好的选择。好在作者也都能积极的响应回复,于是产生了不少精彩的技术讨论。建议中国的程序员们也可以看看国外的开发者们对于这种“吐槽”类文章的态度以及他们讨论问题的方式。

[转载]为什么我希望用C而不是C++来实现ZeroMQ的更多相关文章

  1. 为什么我希望用C而不是C++来实现ZeroMQ

    原文在这里 开始前我要先做个澄清:这篇文章同Linus Torvalds这种死忠C程序员吐槽C++的观点是不同的.在我的整个职业生涯里我都在使用C++,而且现在C++依然是我做大多数项目时的首选编程语 ...

  2. 转载:关于消息队列的使用----ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ

    转载: http://blog.csdn.net/konglongaa/article/details/52208273

  3. 转载--C++的反思

    转载自http://blog.csdn.net/yapian8/article/details/46983319 最近两年 C++又有很多人出来追捧,并且追捧者充满了各种优越感,似乎不写 C++你就一 ...

  4. C++的反思[转]

    最近两年 C++又有很多人出来追捧,并且追捧者充满了各种优越感,似乎不写 C++你就一辈子是低端程序员了,面对这种现象,要不要出来适时的黑一下 C++呢?呵呵呵. 咱们要有点娱乐精神,关于 C++的笑 ...

  5. 【STM32】IIC的基本原理(实例:普通IO口模拟IIC时序读取24C02)(转载)

     版权声明:本文为博主原创文章,允许转载,但希望标注转载来源. https://blog.csdn.net/qq_38410730/article/details/80312357 IIC的基本介绍 ...

  6. 深入理解Flink核心技术(转载)

    作者:李呈祥 Flink项目是大数据处理领域最近冉冉升起的一颗新星,其不同于其他大数据项目的诸多特性吸引了越来越多的人关注Flink项目.本文将深入分析Flink一些关键的技术与特性,希望能够帮助读者 ...

  7. dp题目列表

    此文转载别人,希望自己能够做完这些题目! 1.POJ动态规划题目列表 容易:1018, 1050, 1083, 1088, 1125, 1143, 1157, 1163, 1178, 1179, 11 ...

  8. Apache Spark源码走读之16 -- spark repl实现详解

    欢迎转载,转载请注明出处,徽沪一郎. 概要 之所以对spark shell的内部实现产生兴趣全部缘于好奇代码的编译加载过程,scala是需要编译才能执行的语言,但提供的scala repl可以实现代码 ...

  9. 我们如何学好java

    转载:http://www.zhihu.com/question/19945685 马士兵有一篇文章,我觉得写得很好,原文转载如下,希望题主可以好好读读. ---------------------- ...

随机推荐

  1. 1.html5究竟是什么

    1.html5的起源,历史背景…… 按照一般的套路,我这里应该对html5的起源和发展历史,其优越性等大书特书一番.但既然你有意识地专门去找类似的文章,说明你早有相应的认识,就算没有,类似的东西网上也 ...

  2. pyenv

    export PYTHON_BUILD_MIRROR_URL="http://pyenv.qiniudn.com/pythons/"

  3. Redhat 6环境下安装Oracle 12c的方法

    Step 1: 要在Linux上安装Oracle,需要在安装Oracle之前安装好相应的软件包,在不同操作系统环境下,对软件包的要求各不相同.具体对应的软件包,见官网文档:https://docs.o ...

  4. 为了android sdk下载,必须修改hosts

    #Download 下载 203.208.46.146 dl.google.com 203.208.46.146 dl-ssl.google.com #Groups 203.208.46.146 gr ...

  5. eclipse java 空心J文件的回复

    eclipse中的空心J的java文件,表示当前文件不包含在项目中进行编译,而仅仅是当做资源存在项目中. 解决方案如下: 1.鼠标右击当前空心j文件,-->build path-->inc ...

  6. oracle 查询今天哪个表增加的数据多

    一.创建一个表  create table A(  TABLE_NAME VARCHAR2(200),  COUNT_NUM  NUMBER) 二.创建一个存储过程create or replace  ...

  7. Go语言类型switch

    switch还可以用于判断变量类型.使用方式为T.(type),即在变量后加上.(type).见代码: package main import ( "fmt" ) func mai ...

  8. 从零开始学ios开发(三):第一个有交互的app

    感谢大家的关注,也给我一份动力,让我继续前进.有了自己的家庭有了孩子,过着上有老下有小的生活,能够挤出点时间学习真的很难,每天弄好孩子睡觉已经是晚上10点左右了,然后再弄自己的事情,一转眼很快就到12 ...

  9. MVC4.0 利用HandleErrorAttribute和log4net实现记录异常日志功能

    1.MVC4.0中HandleErrorAttribte已经帮我们处理了异常问题,当我们新建一个非空的MVC项目时候,在FilterConfig中会发现这样的代码 public class Filte ...

  10. FPGA---ucf文件编写

    摘要:本文主要通过一个实例具体介绍ISE中通过编辑UCF文件来对FPGA设计进行约束,主要涉及到的约束包括时钟约束.群组约束.逻辑管脚约束以及物理属性约束. Xilinx FPGA设计约束的分类 Xi ...