[TL]关于C++的一些技术细节

119 views
Skip to first unread message

up duan

unread,
Sep 8, 2011, 10:23:22 PM9/8/11
to TopLanguage
既然我们的祖先已经学会了采矿、冶炼、制斧,那么我们砍树的时候就不应该跑到火山旁边捡两块黑曜岩互相撞击然后选择一个有比较锋锐棱角的作为砍树工具。

故事得从一个论坛中的帖子说起:

template<class T, class Init, class Release>
struct MyRAII{
  T resource;
  Init f;
  Release g;

  MyRAII(typename Init::param x){ resource = f(x); }
  ~MyRAII() { g(resource); }
};

这是Xinyu LIU兄在帖子中给出的如何使用RAII的例子,我说其中有风险,LIU兄推荐我看看Scott Mayers的Effective C++的第二章,手头上暂时没有书就没有看了,不过我仍然坚持我的观点。

且听我一一道来。

虽然我不知道第二章究竟说了些啥,但是我想应该是所谓的对象构造过程中抛出异常的话,那么其析构不会被调用……云云。

很多人看到这儿总是有一股子恶心在胃里蔓延,为啥?C++的规矩太多了,你看你看……,这个。但是且慢,这个很正常,这个规矩非常容易理解,因为析构说到底是一个存放对象销毁时要做一些影响环境的事情的地方。而如果对象构造过程中抛出异常,导致对象没有被完整的构造出来,那么,不完整的甚至不合法的对象怎么被析构?所以这个规矩既必要又合乎逻辑。

有人不满了,面向对象的语言多了,没见过哪个语言有这个要求的。

我从两方面回答这个诘问。

一、大多数拥有GC的OO语言,其实没有析构函数的容身之地的,一个对象死了就死了,由运行时销毁,不需要程序员在里面指示对象死的时候再怎么临死反击,回光返照一下。实际上,在有GC支持的OO语言中,都会有一个很明确的禁令:不要再析构的地方复活一个对象(任意一个,不一定是自己)。为什么?因为对象的依赖图是非常复杂的,GC时,意味着已经分析清楚对象依赖存活图了,死掉的就要被回收了,结果如果又重新改变了对象存活图,这次GC的准备要全部废弃,不能进行任何回收,需要重新分析和整理,然后才能回收。设想这样一种情景:好几个调皮的对象都在自己析构的时候修改了对象存活图,然后在第一GC时第一个对象导致了重新开始,第二次GC时第二个对象有导致重新开始……,第n+1次时第一个对象又导致了重新开始,啊,世世代代无穷匮也。

二、C++在构造对象这件事情上的视角更低,考虑的更精致,这就导致了C++的对象构造不是一个整体的步骤,而是一系列的步骤组成的。任何一个环节出了问题,都会导致构造失败,而这时候未完成对象的状态对于程序员来说是未知的(这就是为什么不应该调用析构的原因,析构函数总是假设对象完整合法,但是这儿不是的,所以不能调用),但是运行时当然知道是怎么回事了。根据对称要求,没有构造出来的对象就不应该被析构。

那么为什么我仍要说LIU兄的实现有危险呢?因为,唉,又是C++的规矩,C++的麻烦似乎就在这儿,无穷无尽的规矩,C++说:对象在构造函数执行到函数体的{时,构造完成。

啊,这是什么意思?怎么这么BT,这简直就是不可理喻啊。

但是还是且慢,我们仍有理由。C++设计的时候有一些指导原则,其中有一个非常重要的就是零负荷(也就是说,不需要为没有用到的功能付出代价),零负荷原则投射到类型系统上的结果就是:C++之父需要“原始权利”。他想让我们设计的类型跟int float char之类的原始类型一样而无二致。就是因为这个诱因和想法,导致了上面说的那个不太可以理喻的规矩。

如果你在玩这个游戏,请你遵守游戏规则,否则会有伤害加身。除非你不玩这个游戏。

C++这个游戏需要遵守的规则太多了,我们必须深刻到理解为什么有这些规则,才能不被它伤害。

我们开始推导过程

先热个身,注意,这儿有装逼倾向,自视清高者不要看。

int i;
int i();
int i(0);
int i = 0;
int i = int();
int i = int(0);

它们有什么差异?如果把int泛化为类型T,可能我们会更直观一些。

这里面引入的概念包括:构造、初始化。其实,初始化并不是构造的一部分,很多人还会进一步联想到赋值这个跟初始化很类似的概念,当然,这更不是构造的一部分了。那么构造究竟干了什么呢?

上面的作为全局或者栈上对象跟堆上对象的关于创建的唯一差别就是没有了内存分配过程。

关于new operator干的事情(也就是创建对象的标准流程),我们都耳熟能详了。首先调用operator new分配内存,然后调用构造函数把这个生鲜的内存活化为一个对象。

构造函数有很多工作要做,逐层的构造对象(由根而父而己……),每个构造对象过程包含了准备一些实现OO特性的基础设施(比如vtbl和vptr的准备等),按照特定顺序逐一构造数据成员(这儿也有可能需要程序员参与,一会儿详细说),然后是用户进行的一些初始化操作。

实际上,对于int这样的类型,其对象的构造过程简单的难以置信——完全没有。由于不是类型体系上的一个节点,所以不会有父类型对象先被构造;由于是一个实类型,不需要OO特征,所以不需要准备基础设施,也由于没有数据成员,不需要做任何事情,所以其构造就是空。

我们再从C++构造函数的语法上看一些端倪。

class B {
public:
    B(int b) {x = b;};
private:
    int x;
};

class C : public B {
public:
    C() : B(0), i(0), j(k) {k = 5;}
private:
    int const i;
    int& j;
    int k;
};

很多人认为C++的语法奇特,尤其是其构造函数,简直就是集恶劣语法之大成。这么说不无道理。上面的代码我尽量写得中规中矩,没有多继承,没有让人崩溃的顺序问题的引入,但是仍然让人感觉嗔目结舌的难以理解。注意,上面的构造函数的初始化列表是不可避免的,我没有为了展现这个语法而故意用初始化语法。恰恰相反,能不用的我都没有用,甚至因此背上了性能代价也在所不惜。

上面就展现了那些需要程序员参与构造的成员变量。这儿牵扯到一个比较复杂的问题,C++某些对象要求构造和初始化必须同时完成。

这是可以理解的……,……

很多同学就不满意了,有什么不是可以理解的呢?难道别的OO语言都是虚拟的存在,就C++才真实存在?别的OO语言那个有这个复杂精致的过了分的语法呢?这里面牵扯到视角的问题。C++的语法希望尽可能的把低层次的实现细节都表达出来,而别的OO语言的语法希望尽可能的吧低层次的实现细节都隐藏起来。故有此差异。至于这两个谁有谁劣,人人心里有一杆秤吧。

在我心里的秤是这样的——为了叙述方便,我们不C++的角度叫做A,别的角度叫做B——A的学习成本太高了,而且一般说来,没有太好的回报,因为经过这么多年的软件实践,已经知道了很多“最佳”方法,而B就是那种可以直接支持“最佳”方法而且不会引入学习成本的方式。但是如果已经在A上投入了大量的学习成本,已经学会了,那么A的优势是,它不像是给你一个万能梯子让你爬上去,而是给了你一套定制梯子的方法让你可以针对特定的情形定制特定的梯子,然后爬上去。当然,所谓B的万能梯子,只是在大多数情况下有效,在某些极限的情况下会失去效用,而逻辑上A的方式可以沿用无穷。

如果要在各个计算机语言间进行对比,甚或要设计一门新的语言(我说的是通用目的的,专用目的的不在此讨论范围内),那么C++一定要参照,其中大量的经验和规矩以及无穷无尽的细节甚至错误都是需要借鉴的宝库。

扯远了,回来……

上面的C++构造的语法【初始化列表】是为了应付两个看似简单的扩展,一个用处很大:const,用来规定一个对象在当前视角下是状态不变的,另一个——引用——主要用于IO中(此IO是指把function作为一个系统,其参数和返回值视作出入接口而言的),用于避免copy构造的调用,其最初的本意其实就是给一个对象一个新名字。

假设上面的class B没有我们写得那个构造函数,那么编译器生成的默认构造函数会是什么样的?回想一下零负荷原则,回想一下“原始权利”,我们可以大胆的推测,class B的默认构造函数啥也没干,是真的啥也没干,甚至都不会被调用,如同int一样!甚至class B对象的大小也会跟int对象的大小一致。这就是为什么有一个优化方式就是把没干活的构造函数删掉。

但是既然有了我们自己写的构造函数,我们就分析一下,这个构造函数干了些什么?接受一个参数,并在函数体内用这个参数给自己的数据成员赋值。

请注意我上面的措辞,我说的是赋值。没错,这儿不是初始化,是赋值。

int i;
i = 3;
这就是赋值,初始化能且只能在对象构造时同时进行,虽然初始化并不是构造。请再次回忆原始类型的构造。

再次重复一下,C++要求某些对象的构造和初始化必须同时完成,但是构造和初始化仍然是两回事。

至于这个变态的要求来自何处,其实这并不是变态,来源和很自然和直接。比如:既然对于const的定义是一个不能修改的对象,那就意味着一旦构造出来这个对象,我们就没有机会进行状态更新操作。所以,我们要求构造对象的同时完成对象的初始化过程(由于初始化最多仅有一次),以便能构造出我们需要的对象,这样就满足了const的语义要求了。而引用跟其目标也是一种终生不变的联姻,所以也需要在构造的时候初始化。

而上面的class B的构造函数,在{处,对象已经构造完善,其成员x也构造完成,没有初始化,其值任意,{后}前,对成员x进行了赋值。注意仔细的看这段语句,一个对象出现在程序中,必须有一个构造的过程,但是可以没有初始化的过程。

至此已经清楚了,构造函数{}内部的语句抛出异常,并不会导致构造失败。那只是构造完了之后干某些事情失败了而已。如果此异常导致了对象析构,析构函数照样会被调用。

建筑之道,在乎自然。虽说古人说过顺为人,逆为仙,但毕竟仙太少了,做个好人就不错了。

我还是回到我最愿意用的例子来说吧。

ProcessResult f(char* filename) {
    ProcessResult result = 0;//ProcessResult::Empty;
    FILE* file = fopen(filename, "rb");
    if (file) {
        fseek(file, 0, SEEK_END);
        long fileLength = ftell(file);
        unsigned char* buffer = (unsigned char*)malloc(fileLength * sizeof(unsigned char));
        fseek(file, 0, SEEK_BEGIN);
        size_t length = fread(buffer, sizeof(unsigned char), fileLength, file);
        if (length == fileLength) {
            ParseResult* parseResult = parse(buffer, length);
            if (parseResult) {
                result = process(parseResult);
                destroyParse(parseResult);
            } else {
                //parse error
            }
        } else {
            //read file error
        }
        free(buffer);
        fclose(file);
    } else {
        //file open error
    }
    return result;
}

对于这个例子,最好的也最能体现exception优势的方案是这样的:

ProcessResult f(char* filename) {
    ProcessResult result = 0;//ProcessResult::Empty;
    try {
        FILE* file = fopen(filename, "rb");
        fseek(file, 0, SEEK_END);
        long fileLength = ftell(file);
        unsigned char* buffer = (unsigned char*)malloc(fileLength * sizeof(unsigned char));
        fseek(file, 0, SEEK_BEGIN);
        size_t length = fread(buffer, sizeof(unsigned char), fileLength, file);
        ParseResult* parseResult = parse(buffer, length);
        result = process(parseResult);
        destroyParse(parseResult);
        free(buffer);
        fclose(file);
    } catch (ParseException& pe) {
        //parse error
    } catch (FileReadException& fre) {
        //read file error
    } catch (FileOpenException& foe) {
        //file open error
    }
    return result;
}

但是这需要有前提条件:一、异常,二、RAII。
如果只有其一没有其二,则异常处理中释放资源的代码会不断的重复。当然,如果有finally,情况似乎会好一点,但是我还是不乐观,因为即便有finally做资源释放,其try也得是层次嵌套的(类似于if层次嵌套),而不能像有RAII辅助的代码段那样,业务逻辑丝毫不被打断。

无论是第一个还是第二个,实现起来都很麻烦。C++语言虽说都做了支持,但都没有做到if那个级别,我记得以前TL讨论过for和for_each的优劣问题,其实归根结蒂就是支持的强度问题。支持的力度不够,就会导致使用起来复杂度大,而实际上异常和RAII都是复杂的机制,使用起来都比较难受,C++之父也常感慨没有一个好的展现异常如何被使用的库。

这都是我要说的和谐的引言。

明眼人能看出来,采用异常的方案是不可运行的,只是伪代码。原因很简单。它依赖的那些基础设施并不满足采用这个方案的基本要求。比如:fopen打开失败后不是返回一个NULL的FILE指针,而是抛出一个FileOpenException,并且,FILE本身不是一个普通的结构,而是一个符合RAII要求的类。等等……,这都是麻烦,这都不是真的,这意味着:基于异常和RAII的方案对环境的假设跟现实社会中的大量存在的代码基的现状并不匹配。

最后,我还想说说Xinyu LIU兄那个MyRAII类我认为的潜在的问题,只是我一家之言,供参考。
可商榷之一:委托MyRAII进行管理的Resource,必须同时把自己的Init和Release给出。
可商榷之二:MyRAII对Init的要求太强,不像是一个可以通用的库,而且由于Resource的构建参数都是经由MyRAII的构造传进去的,这导致耦合性大增而灵活可重用性大减。

其实RAII虽说直译为资源获取即初始化,但后面那个没有说出来的部分才是RAII提供的精髓的价值。这其实就是智能指针干的事。如果这样定位的话,我们同时也发觉LIU兄没有采用Scott mayers击节赞叹的boost智能指针销毁持有物而不增加模板参数的技巧,我是觉得那个技巧非常好。

一旦思路转换到这儿,我们就能发现,只要用好智能指针,RAII实现起来麻烦程度就降低了不少。不过还有一些让人难受的细节作祟,而据说魔鬼在细节里藏身。

比如:
struct FileClose {
    operator()(FILE* file) {
        fclose(file);
    }
};

shared_ptr<FILE> file(fopen(filename, "rb"), FileClose());
看起来不错啊。唯一的瑕疵是FileClose的存在,不过有了lambda,这会解决的。只可惜……

还有一点点小瑕疵。shared_ptr要求第一个参数是有效指针,而fopen是通过返回无效指针来表示失败的,这里面会不会有问题呢?谁能说清楚给它一个无效指针它会有什么奇异的反应呢?

我们没有把握。只好:
FILE* temp = fopen(filename, "rb");
shared_ptr<FILE> file((temp ? temp : throw Exception()), FileClose());

代码一下子就不美观了。或许还可以在FileClose里面加上一个判断if(file) fclose(file);其实这只是拖延战术。我们总要用file这个RAII化的资源吧,用的过程中才出错比打开失败还要差。

更何况,还有一个大的原因就是:我们调用的代码基是C风格的,需要的都是赤裸裸的指针,这就要求我们不断的get,而且,C风格的代码大都用返回值代表执行状态,而执行的结果却在出口参数中,这会导致C和智能指针的不断的交叠,代码非常难看。

为什么?原因还是一样的,我们不应该在山上养鱼,在水里面养蝎子。我们应该因时因地制宜,面对的资源是C接口的时候,用尽心力去实现一个不和谐的RAII或者异常接口,并不是很明智,而且会导致非常难以理解的代码出现。

当然,更差的是我们没有用shared_ptr而是自己实现了一个不太完善不太合理的定制版,正如同我们弃用钢斧而用一个很蹩脚的容易伤到自己受的石斧去砍树一样。

本来想说C++中有很多技术细节,如果没有留心或者不知道,很容易出现误伤自己的局面,没想到唠唠叨叨说了这么多。

jiang yu

unread,
Sep 8, 2011, 10:56:50 PM9/8/11
to pon...@googlegroups.com
Effect++好像说过(要不就是exception c++)勿在构造函数中抛出异常,所以这个struct MyRAII 有问题。
google的c++代码规范明确的说构造函数别复杂,宁可用一个init来替代(不是构造中调init啊,而是外部调)
关于RAII和异常,能正确书写的人不多,所以google也明确的抛弃了异常,至于RAII,好用就用,不好用也勿强求。

up duan

unread,
Sep 9, 2011, 12:09:10 AM9/9/11
to pon...@googlegroups.com


2011/9/9 jiang yu <yu.jia...@gmail.com>

Effect++好像说过(要不就是exception c++)勿在构造函数中抛出异常,所以这个struct MyRAII 有问题。
google的c++代码规范明确的说构造函数别复杂,宁可用一个init来替代(不是构造中调init啊,而是外部调)
类似于MFC采用的两阶段构造?个人感觉这个在逻辑上导致了不能维持类的不变量,不是一个好的办法。
C++构造的复杂在于需要Compiler和Programmer配合,各司其职, 而又把这一切放在一个地方,而且还有限度的允许Programmer对Compiler完成的那部分进行修订。

jiang yu

unread,
Sep 9, 2011, 12:22:31 AM9/9/11
to pon...@googlegroups.com

Doing Work in Constructors

link
In general, constructors should merely set member variables to their initial values. Any complex initialization should go in an explicit Init() method.

Definition:It is possible to perform initialization in the body of the constructor.

Pros:Convenience in typing. No need to worry about whether the class has been initialized or not.

Cons:The problems with doing work in constructors are:

  • There is no easy way for constructors to signal errors, short of using exceptions (which are forbidden).
  • If the work fails, we now have an object whose initialization code failed, so it may be an indeterminate state.
  • If the work calls virtual functions, these calls will not get dispatched to the subclass implementations. Future modification to your class can quietly introduce this problem even if your class is not currently subclassed, causing much confusion.
  • If someone creates a global variable of this type (which is against the rules, but still), the constructor code will be called before main(), possibly breaking some implicit assumptions in the constructor code. For instance,gflags will not yet have been initialized.

Decision:If your object requires non-trivial initialization, consider having an explicit Init() method. In particular, constructors should not call virtual functions, attempt to raise errors, access potentially uninitialized global variables, etc.

qiaojie

unread,
Sep 9, 2011, 12:36:26 AM9/9/11
to pon...@googlegroups.com
这篇写的不错,不过那么多内容没法一一展开探讨了。

仅就这段发表一下看法:
ProcessResult f(char* filename) {
    ProcessResult result = 0;//ProcessResult::Empty;
    try {
        FILE* file = fopen(filename, "rb");
        fseek(file, 0, SEEK_END);
        long fileLength = ftell(file);
        unsigned char* buffer = (unsigned char*)malloc(fileLength * sizeof(unsigned char));
        fseek(file, 0, SEEK_BEGIN);
        size_t length = fread(buffer, sizeof(unsigned char), fileLength, file);
        ParseResult* parseResult = parse(buffer, length);
        result = process(parseResult);
        destroyParse(parseResult);
        free(buffer);
        fclose(file);
    } catch (ParseException& pe) {
        //parse error
    } catch (FileReadException& fre) {
        //read file error
    } catch (FileOpenException& foe) {
        //file open error
    }
    return result;
}

我想说的是这段处理方法还是停留在错误返回值的思路上,而不是异常的最佳实践。
引入异常的初衷就是让使用者从逐层的判断错误以及每个具体错误的判断中解脱出来。90%以上的错误(比方说文件打不开,文件格式错误,网络中断等)是不需要程序自己去处理和恢复的。之所以程序要检查错误的原因有2点,第一是要向用户汇报错误原因,第二是要让程序优雅的结束或者重试,而不会因为错误而失去控制。要做到上述2点,使用异常的话就会非常方便,第一汇报错误的话我们不需要去catch具体的错误类型,只要catch(exception& e),然后print e.what()就可以了,第二要让程序优雅的结束或者重试的话,我们只需要在最上层的用户发起操作请求的函数里进行异常捕获,向用户汇报之后选择结束或者重试就可以了。所以上面这里例子里,我们不应该在f(char* filename)函数里捕获异常,而是应该在更外层的函数里处理,而且也不需要去处理具体的错误类型,因为也没什么好处理的。但是,这个例子里涉及到一个异常安全的问题,在f函数里抛出异常如果我们不去捕获的话,我们没法把资源清理掉。这主要是因为C++不支持finally,C++给出的解法是用RAII,所以如果C++里面用异常又不用RAII的话就会出现大问题,一个不太优雅的方式是catch(exception& e){cleanup; throw e;} 来模拟finally。






2011/9/9 up duan <fix...@gmail.com>

Shuo Chen

unread,
Sep 9, 2011, 1:04:58 AM9/9/11
to TopLanguage
> catch(exception& e){cleanup; throw e;} 来模拟finally。

这是错的,应该用 throw; 代替 throw e;

> 2011/9/9 up duan <fixo...@gmail.com>


>
> > 既然我们的祖先已经学会了采矿、冶炼、制斧,那么我们砍树的时候就不应该跑到火山旁边捡两块黑曜岩互相撞击然后选择一个有比较锋锐棱角的作为砍树工具。
>
> > 故事得从一个论坛中的帖子说起:
>
> > template<class T, class Init, class Release>
> > struct MyRAII{
> > T resource;
> > Init f;
> > Release g;
>
> > MyRAII(typename Init::param x){ resource = f(x); }
> > ~MyRAII() { g(resource); }
> > };
>
> > 这是Xinyu LIU兄在帖子中给出的如何使用RAII的例子,我说其中有风险,LIU兄推荐我看看Scott Mayers的Effective
> > C++的第二章,手头上暂时没有书就没有看了,不过我仍然坚持我的观点。
>
> > 且听我一一道来。
>

> > 虽然我不知道第二章究竟说了些啥,但是我想应该是所谓的对象构造过程中抛出异常的话,那么其析构不会被调用......云云。
>
> > 很多人看到这儿总是有一股子恶心在胃里蔓延,为啥?C++的规矩太多了,你看你看......,这个。但是且慢,这个很正常,这个规矩非常容易理解,因为析构说到底是一个存放对象销毁时要做一些影响环境的事情的地方。而如果对象构造过程中抛出异常,导致对象没有被完整的构造出来,那么,不完整的甚至不合法的对象怎么被析构?所以这个规矩既必要又合乎逻辑。
>
> > 有人不满了,面向对象的语言多了,没见过哪个语言有这个要求的。
>
> > 我从两方面回答这个诘问。
>
> > 一、大多数拥有GC的OO语言,其实没有析构函数的容身之地的,一个对象死了就死了,由运行时销毁,不需要程序员在里面指示对象死的时候再怎么临死反击,回光返照一下。实际上,在有GC支持的OO语言中,都会有一个很明确的禁令:不要再析构的地方复活一个对象(任意一个,不一定是自己)。为什么?因为对象的依赖图是非常复杂的,GC时,意味着已经分析清楚对象依赖存活图了,死掉的就要被回收了,结果如果又重新改变了对象存活图,这次GC的准备要全部废弃,不能进行任何回收,需要重新分析和整理,然后才能回收。设想这样一种情景:好几个调皮的对象都在自己析构的时候修改了对象存活图,然后在第一GC时第一个对象导致了重新开始,第二次GC时第二个对象有导致重新开始......,第n+1次时第一个对象又导致了重新开始,啊,世世代代无穷匮也。


>
> > 二、C++在构造对象这件事情上的视角更低,考虑的更精致,这就导致了C++的对象构造不是一个整体的步骤,而是一系列的步骤组成的。任何一个环节出了问题,都会导致构造失败,而这时候未完成对象的状态对于程序员来说是未知的(这就是为什么不应该调用析构的原因,析构函数总是假设对象完整合法,但是这儿不是的,所以不能调用),但是运行时当然知道是怎么回事了。根据对称要求,没有构造出来的对象就不应该被析构。
>
> > 那么为什么我仍要说LIU兄的实现有危险呢?因为,唉,又是C++的规矩,C++的麻烦似乎就在这儿,无穷无尽的规矩,C++说:对象在构造函数执行到函数体的{时,构造完成。
>
> > 啊,这是什么意思?怎么这么BT,这简直就是不可理喻啊。
>
> > 但是还是且慢,我们仍有理由。C++设计的时候有一些指导原则,其中有一个非常重要的就是零负荷(也就是说,不需要为没有用到的功能付出代价),零负荷原则投射到类型系统上的结果就是:C++之父需要"原始权利"。他想让我们设计的类型跟int
> > float char之类的原始类型一样而无二致。就是因为这个诱因和想法,导致了上面说的那个不太可以理喻的规矩。
>
> > 如果你在玩这个游戏,请你遵守游戏规则,否则会有伤害加身。除非你不玩这个游戏。
>
> > C++这个游戏需要遵守的规则太多了,我们必须深刻到理解为什么有这些规则,才能不被它伤害。
>
> > 我们开始推导过程
>
> > 先热个身,注意,这儿有装逼倾向,自视清高者不要看。
>
> > int i;
> > int i();
> > int i(0);
> > int i = 0;
> > int i = int();
> > int i = int(0);
>
> > 它们有什么差异?如果把int泛化为类型T,可能我们会更直观一些。
>
> > 这里面引入的概念包括:构造、初始化。其实,初始化并不是构造的一部分,很多人还会进一步联想到赋值这个跟初始化很类似的概念,当然,这更不是构造的一部分了。那么构造究竟干了什么呢?
>
> > 上面的作为全局或者栈上对象跟堆上对象的关于创建的唯一差别就是没有了内存分配过程。
>
> > 关于new operator干的事情(也就是创建对象的标准流程),我们都耳熟能详了。首先调用operator
> > new分配内存,然后调用构造函数把这个生鲜的内存活化为一个对象。
>

> > 构造函数有很多工作要做,逐层的构造对象(由根而父而己......),每个构造对象过程包含了准备一些实现OO特性的基础设施(比如vtbl和vptr的准备等),按照特定顺序逐一构造数据成员(这儿也有可能需要程序员参与,一会儿详细说),然后是用户进行的一些初始化操作。
>
> > 实际上,对于int这样的类型,其对象的构造过程简单的难以置信----完全没有。由于不是类型体系上的一个节点,所以不会有父类型对象先被构造;由于是一个实类型,不需要OO特征,所以不需要准备基础设施,也由于没有数据成员,不需要做任何事情,所以其构造就是空。


>
> > 我们再从C++构造函数的语法上看一些端倪。
>
> > class B {
> > public:
> > B(int b) {x = b;};
> > private:
> > int x;
> > };
>
> > class C : public B {
> > public:
> > C() : B(0), i(0), j(k) {k = 5;}
> > private:
> > int const i;
> > int& j;
> > int k;
> > };
>
> > 很多人认为C++的语法奇特,尤其是其构造函数,简直就是集恶劣语法之大成。这么说不无道理。上面的代码我尽量写得中规中矩,没有多继承,没有让人崩溃的顺序问题的引入,但是仍然让人感觉嗔目结舌的难以理解。注意,上面的构造函数的初始化列表是不可避免的,我没有为了展现这个语法而故意用初始化语法。恰恰相反,能不用的我都没有用,甚至因此背上了性能代价也在所不惜。
>
> > 上面就展现了那些需要程序员参与构造的成员变量。这儿牵扯到一个比较复杂的问题,C++某些对象要求构造和初始化必须同时完成。
>

> > 这是可以理解的......,......


>
> > 很多同学就不满意了,有什么不是可以理解的呢?难道别的OO语言都是虚拟的存在,就C++才真实存在?别的OO语言那个有这个复杂精致的过了分的语法呢?这里面牵扯到视角的问题。C++的语法希望尽可能的把低层次的实现细节都表达出来,而别的OO语言的语法希望尽可能的吧低层次的实现细节都隐藏起来。故有此差异。至于这两个谁有谁劣,人人心里有一杆秤吧。
>

> ...
>
> read more >>

up duan

unread,
Sep 9, 2011, 1:20:50 AM9/9/11
to pon...@googlegroups.com


2011/9/9 qiaojie <qia...@gmail.com>
说的有道理。我也知道异常要用在longjmp的场景下才是正常的用法,我这样写主要是为了对照嵌套if,因为那个版本是完全在本地处理了的。
另外,即便支持finally,也要嵌套try finally才能正常的正常的完成嵌套if表达的内容,除非支持RAII。 

Yongwei Wu

unread,
Sep 10, 2011, 10:39:28 AM9/10/11
to pon...@googlegroups.com
这是Google不用异常带来的恶心的历史遗留问题。由于Google对C++异常不感冒,Android对C++异常的支持最近才加上,在NDK r6b之前几乎无法正常使用。Boost使用也有不少问题。

如果你注意的话,在Google的C++风格指南中还能看到这段话(我加红了):

Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we'd like to use our open-source projects at Google and it's difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well. Things would probably be different if we had to do it all over again from scratch.

2011/9/9 jiang yu <yu.jia...@gmail.com>



--
Wu Yongwei
URL: http://wyw.dcweb.cn:8001/ (Temporary)

Yongwei Wu

unread,
Sep 10, 2011, 10:58:38 AM9/10/11
to pon...@googlegroups.com
2011/9/9 up duan <fix...@gmail.com>:

> 那么为什么我仍要说LIU兄的实现有危险呢?因为,唉,又是C++的规矩,C++的麻烦似乎就在这儿,无穷无尽的规矩,C++说:对象在构造函数执行到函数体的{时,构造完成。

不知道你想说明啥。至少上面的“C++说”是错误的。我想你把什么搞混了吧。

见下面的测试代码:

#include <iostream>
#include <stdexcept>

using namespace std;

class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
private:
A(const A&);
A& operator=(const A&);
};

class B {
public:
B();
~B();
private:
A a_;
};

B::B()
{
cout << "B()" << endl;
throw runtime_error("Exception from B");
}

B::~B()
{
cout << "~B()" << endl;
}

int main()
{
try {
B* p = new B();
} catch (exception& e) {
cout << "Exception caught: " << e.what() << endl;
}
}

运行结果:

A()
B()
~A()
Exception caught: Exception from B

在执行到B()的{时,A已经构造完成。在执行到B的}之前,B没有构造完成。所以异常发生时,A会被析构;但B因为还没构造完成,不会调用~B()。

如果把你那句话换成这样,就可以了:成员对象在容器对象的构造函数执行到函数体的{时,构造完成。

但这是你的意思吗?字面差异也太大了吧。

jiang yu

unread,
Sep 11, 2011, 12:05:18 AM9/11/11
to pon...@googlegroups.com
有多少往事可以重来?我想,这不过是google诗意的表达罢了。

yuan zhu

unread,
Sep 11, 2011, 5:16:31 AM9/11/11
to pon...@googlegroups.com
在一个没有垃圾回收的语言里不用异常要死人吗?就好比和中国人谈话不夹杂点英文会哽死吗?

硬要翻出康熙字典,挑出那些已经被人们实践中抛弃的字眼,来掉书袋很好玩吗?

且不说C++异常在混合语言编程时的各种不便,光光跨动态库时异常带来的各种灾难就足以够你喝一壶了,更不要说楼上那个a_在析构时如果崩溃,栈顶是额外的一帧对DUMP调试带来的麻烦了。

没有异常,C++其实挺好的。就因为赶时髦,最后搞得来鸡犬不类。

2011/9/11 jiang yu <yu.jia...@gmail.com>

up duan

unread,
Sep 11, 2011, 7:18:45 AM9/11/11
to pon...@googlegroups.com
给出这样的实例时,请给出编译器型号和版本。

2011/9/10 Yongwei Wu <wuyo...@gmail.com>

up duan

unread,
Sep 11, 2011, 7:23:36 AM9/11/11
to pon...@googlegroups.com
另外,毫无必要的在你的测试代码中引入cout,这很危险。关于cout的各种灵异事件或许你没有听过,但是我告诉你:全局兑现的析构兑上带缓存的输出会产生罪恶之花的。

2011/9/11 up duan <fix...@gmail.com>

Yongwei Wu

unread,
Sep 11, 2011, 8:14:39 AM9/11/11
to pon...@googlegroups.com
负责任的说,你能找出一个最近三年发布的C++编译器结果不同,我也就服你了。

我用的是GCC 4.0/4.2/4.5,刚才还在CodePad上试过:

http://codepad.org/GONVyesW

2011/9/11 up duan <fix...@gmail.com>:

Yongwei Wu

unread,
Sep 11, 2011, 8:19:48 AM9/11/11
to pon...@googlegroups.com
测试代码用cout不是很正常吗?非得用类型不安全的printf?笑话。何况我这儿根本不研究全局的析构。

即使在全局对象的析构函数里,编译器很多仍然能保证你试用cout的安全性。比如,MSVC有不同优先级的初始化段:

http://support.microsoft.com/kb/104248

你的正常代码根本不会有任何问题。虽说,在这种情况下,为保险起见,我也是使用printf。一般情况则无此必要。

2011/9/11 up duan <fix...@gmail.com>:

Yongwei Wu

unread,
Sep 11, 2011, 9:46:45 AM9/11/11
to pon...@googlegroups.com
我还是太保守了。找到了一台机器装着已经13岁的Visual Studio '98,MSVC的结果仍然是一样的。

2011/9/11 Yongwei Wu <wuyo...@gmail.com>:

Yongwei Wu

unread,
Sep 11, 2011, 9:59:58 AM9/11/11
to pon...@googlegroups.com
没有异常,你就只能利用函数的返回值来表示错误,也就意味着你几乎不可能让一个函数返回一个对象,也就意味着STL和Boost变得几乎不可能。也就是说,C++就根本不是今天的C++了。

混合语言编程不是问题,因为异常本来就该是C++语言内部使用的。你会想让Java的异常抛到C++里去吗?动态库也类似,这是一个设计问题。至于析构时崩溃就更扯了,任何C++的书都会告诉你析构时不允许抛任何异常。

如果你说C++没有STL和Boost也挺好,我只能说,这不是C++社区的主流观念。建议你考虑Objective-C,简单,没有异常,目前流行程度也不错,只是结果程序多半只能跑在苹果的平台上。

2011/9/11 yuan zhu <zy49...@gmail.com>:

>>>>>>> 虽然我不知道第二章究竟说了些啥,但是我想应该是所谓的对象构造过程中抛出异常的话,那么其析构不会被调用......云云。
>>>>>>>
>>>>>>>
>>>>>>> 很多人看到这儿总是有一股子恶心在胃里蔓延,为啥?C++的规矩太多了,你看你看......,这个。但是且慢,这个很正常,这个规矩非常容易理解,因为析构说到底是一个存放对象销毁时要做一些影响环境的事情的地方。而如果对象构造过程中抛出异常,导致对象没有被完整的构造出来,那么,不完整的甚至不合法的对象怎么被析构?所以这个规矩既必要又合乎逻辑。
>>>>>>>
>>>>>>> 有人不满了,面向对象的语言多了,没见过哪个语言有这个要求的。
>>>>>>>
>>>>>>> 我从两方面回答这个诘问。
>>>>>>>
>>>>>>>
>>>>>>> 一、大多数拥有GC的OO语言,其实没有析构函数的容身之地的,一个对象死了就死了,由运行时销毁,不需要程序员在里面指示对象死的时候再怎么临死反击,回光返照一下。实际上,在有GC支持的OO语言中,都会有一个很明确的禁令:不要再析构的地方复活一个对象(任意一个,不一定是自己)。为什么?因为对象的依赖图是非常复杂的,GC时,意味着已经分析清楚对象依赖存活图了,死掉的就要被回收了,结果如果又重新改变了对象存活图,这次GC的准备要全部废弃,不能进行任何回收,需要重新分析和整理,然后才能回收。设想这样一种情景:好几个调皮的对象都在自己析构的时候修改了对象存活图,然后在第一GC时第一个对象导致了重新开始,第二次GC时第二个对象有导致重新开始......,第n+1次时第一个对象又导致了重新开始,啊,世世代代无穷匮也。


>>>>>>>
>>>>>>>
>>>>>>> 二、C++在构造对象这件事情上的视角更低,考虑的更精致,这就导致了C++的对象构造不是一个整体的步骤,而是一系列的步骤组成的。任何一个环节出了问题,都会导致构造失败,而这时候未完成对象的状态对于程序员来说是未知的(这就是为什么不应该调用析构的原因,析构函数总是假设对象完整合法,但是这儿不是的,所以不能调用),但是运行时当然知道是怎么回事了。根据对称要求,没有构造出来的对象就不应该被析构。
>>>>>>>
>>>>>>>
>>>>>>> 那么为什么我仍要说LIU兄的实现有危险呢?因为,唉,又是C++的规矩,C++的麻烦似乎就在这儿,无穷无尽的规矩,C++说:对象在构造函数执行到函数体的{时,构造完成。
>>>>>>>
>>>>>>> 啊,这是什么意思?怎么这么BT,这简直就是不可理喻啊。
>>>>>>>
>>>>>>>
>>>>>>> 但是还是且慢,我们仍有理由。C++设计的时候有一些指导原则,其中有一个非常重要的就是零负荷(也就是说,不需要为没有用到的功能付出代价),零负荷原则投射到类型系统上的结果就是:C++之父需要"原始权利"。他想让我们设计的类型跟int
>>>>>>> float char之类的原始类型一样而无二致。就是因为这个诱因和想法,导致了上面说的那个不太可以理喻的规矩。
>>>>>>>
>>>>>>> 如果你在玩这个游戏,请你遵守游戏规则,否则会有伤害加身。除非你不玩这个游戏。
>>>>>>>
>>>>>>> C++这个游戏需要遵守的规则太多了,我们必须深刻到理解为什么有这些规则,才能不被它伤害。
>>>>>>>
>>>>>>> 我们开始推导过程
>>>>>>>
>>>>>>> 先热个身,注意,这儿有装逼倾向,自视清高者不要看。
>>>>>>>
>>>>>>> int i;
>>>>>>> int i();
>>>>>>> int i(0);
>>>>>>> int i = 0;
>>>>>>> int i = int();
>>>>>>> int i = int(0);
>>>>>>>
>>>>>>> 它们有什么差异?如果把int泛化为类型T,可能我们会更直观一些。
>>>>>>>
>>>>>>>
>>>>>>> 这里面引入的概念包括:构造、初始化。其实,初始化并不是构造的一部分,很多人还会进一步联想到赋值这个跟初始化很类似的概念,当然,这更不是构造的一部分了。那么构造究竟干了什么呢?
>>>>>>>
>>>>>>> 上面的作为全局或者栈上对象跟堆上对象的关于创建的唯一差别就是没有了内存分配过程。
>>>>>>>
>>>>>>> 关于new operator干的事情(也就是创建对象的标准流程),我们都耳熟能详了。首先调用operator
>>>>>>> new分配内存,然后调用构造函数把这个生鲜的内存活化为一个对象。
>>>>>>>
>>>>>>>

>>>>>>> 构造函数有很多工作要做,逐层的构造对象(由根而父而己......),每个构造对象过程包含了准备一些实现OO特性的基础设施(比如vtbl和vptr的准备等),按照特定顺序逐一构造数据成员(这儿也有可能需要程序员参与,一会儿详细说),然后是用户进行的一些初始化操作。
>>>>>>>
>>>>>>>
>>>>>>> 实际上,对于int这样的类型,其对象的构造过程简单的难以置信----完全没有。由于不是类型体系上的一个节点,所以不会有父类型对象先被构造;由于是一个实类型,不需要OO特征,所以不需要准备基础设施,也由于没有数据成员,不需要做任何事情,所以其构造就是空。


>>>>>>>
>>>>>>> 我们再从C++构造函数的语法上看一些端倪。
>>>>>>>
>>>>>>> class B {
>>>>>>> public:
>>>>>>> B(int b) {x = b;};
>>>>>>> private:
>>>>>>> int x;
>>>>>>> };
>>>>>>>
>>>>>>> class C : public B {
>>>>>>> public:
>>>>>>> C() : B(0), i(0), j(k) {k = 5;}
>>>>>>> private:
>>>>>>> int const i;
>>>>>>> int& j;
>>>>>>> int k;
>>>>>>> };
>>>>>>>
>>>>>>>
>>>>>>> 很多人认为C++的语法奇特,尤其是其构造函数,简直就是集恶劣语法之大成。这么说不无道理。上面的代码我尽量写得中规中矩,没有多继承,没有让人崩溃的顺序问题的引入,但是仍然让人感觉嗔目结舌的难以理解。注意,上面的构造函数的初始化列表是不可避免的,我没有为了展现这个语法而故意用初始化语法。恰恰相反,能不用的我都没有用,甚至因此背上了性能代价也在所不惜。
>>>>>>>
>>>>>>> 上面就展现了那些需要程序员参与构造的成员变量。这儿牵扯到一个比较复杂的问题,C++某些对象要求构造和初始化必须同时完成。

>>>>>>> 这是可以理解的......,......


>>>>>>>
>>>>>>>
>>>>>>> 很多同学就不满意了,有什么不是可以理解的呢?难道别的OO语言都是虚拟的存在,就C++才真实存在?别的OO语言那个有这个复杂精致的过了分的语法呢?这里面牵扯到视角的问题。C++的语法希望尽可能的把低层次的实现细节都表达出来,而别的OO语言的语法希望尽可能的吧低层次的实现细节都隐藏起来。故有此差异。至于这两个谁有谁劣,人人心里有一杆秤吧。
>>>>>>>
>>>>>>>

>>>>>>> 在我心里的秤是这样的----为了叙述方便,我们不C++的角度叫做A,别的角度叫做B----A的学习成本太高了,而且一般说来,没有太好的回报,因为经过这么多年的软件实践,已经知道了很多"最佳"方法,而B就是那种可以直接支持"最佳"方法而且不会引入学习成本的方式。但是如果已经在A上投入了大量的学习成本,已经学会了,那么A的优势是,它不像是给你一个万能梯子让你爬上去,而是给了你一套定制梯子的方法让你可以针对特定的情形定制特定的梯子,然后爬上去。当然,所谓B的万能梯子,只是在大多数情况下有效,在某些极限的情况下会失去效用,而逻辑上A的方式可以沿用无穷。


>>>>>>>
>>>>>>>
>>>>>>> 如果要在各个计算机语言间进行对比,甚或要设计一门新的语言(我说的是通用目的的,专用目的的不在此讨论范围内),那么C++一定要参照,其中大量的经验和规矩以及无穷无尽的细节甚至错误都是需要借鉴的宝库。
>>>>>>>

>>>>>>> 扯远了,回来......
>>>>>>>
>>>>>>>
>>>>>>> 上面的C++构造的语法【初始化列表】是为了应付两个看似简单的扩展,一个用处很大:const,用来规定一个对象在当前视角下是状态不变的,另一个----引用----主要用于IO中(此IO是指把function作为一个系统,其参数和返回值视作出入接口而言的),用于避免copy构造的调用,其最初的本意其实就是给一个对象一个新名字。

>>>>>>> 明眼人能看出来,采用异常的方案是不可运行的,只是伪代码。原因很简单。它依赖的那些基础设施并不满足采用这个方案的基本要求。比如:fopen打开失败后不是返回一个NULL的FILE指针,而是抛出一个FileOpenException,并且,FILE本身不是一个普通的结构,而是一个符合RAII要求的类。等等......,这都是麻烦,这都不是真的,这意味着:基于异常和RAII的方案对环境的假设跟现实社会中的大量存在的代码基的现状并不匹配。


>>>>>>>
>>>>>>> 最后,我还想说说Xinyu LIU兄那个MyRAII类我认为的潜在的问题,只是我一家之言,供参考。
>>>>>>> 可商榷之一:委托MyRAII进行管理的Resource,必须同时把自己的Init和Release给出。
>>>>>>>
>>>>>>> 可商榷之二:MyRAII对Init的要求太强,不像是一个可以通用的库,而且由于Resource的构建参数都是经由MyRAII的构造传进去的,这导致耦合性大增而灵活可重用性大减。
>>>>>>>
>>>>>>>
>>>>>>> 其实RAII虽说直译为资源获取即初始化,但后面那个没有说出来的部分才是RAII提供的精髓的价值。这其实就是智能指针干的事。如果这样定位的话,我们同时也发觉LIU兄没有采用Scott
>>>>>>> mayers击节赞叹的boost智能指针销毁持有物而不增加模板参数的技巧,我是觉得那个技巧非常好。
>>>>>>>
>>>>>>>
>>>>>>> 一旦思路转换到这儿,我们就能发现,只要用好智能指针,RAII实现起来麻烦程度就降低了不少。不过还有一些让人难受的细节作祟,而据说魔鬼在细节里藏身。
>>>>>>>
>>>>>>> 比如:
>>>>>>> struct FileClose {
>>>>>>> operator()(FILE* file) {
>>>>>>> fclose(file);
>>>>>>> }
>>>>>>> };
>>>>>>>
>>>>>>> shared_ptr<FILE> file(fopen(filename, "rb"), FileClose());

>>>>>>> 看起来不错啊。唯一的瑕疵是FileClose的存在,不过有了lambda,这会解决的。只可惜......

Shuo Chen

unread,
Sep 11, 2011, 10:30:26 AM9/11/11
to TopLanguage
任何一个支持异常的 C++ 编译器都应该是这个结果。

这里 A 是 B 的成员,B::B() 在函数体内抛异常,A 对象的构造是在初始化列表里完成的,
当进入 B::B() 函数体时,A 已经是一个完整的对象,因此 B::B() 抛异常的时候会被析构。

还可以试试
1. B 是 A 的派生类,B::B() 在函数体内抛异常,会引起 A 被析构
2. B 有 A1 和 A2 两个成员,依序排列,A2::A2() 抛异常,会引起 A1 被析构;附加题:如何 catch 这个异常,比较它发
生在函数体之外?catch 住了能干什么?能不能继续构造 B ?
3. A 和 B 是基类,C 多重继承 A 和 B,依次顺序,B::B() 或 C::C() 其中一个抛异常,A 会被析构。
4. A 是基类,B C 虚拟继承 A,D 多重虚拟继承 B 和 C,然后 A::A() 或 B::B() 或 C::C() 其中一个抛异常,
看看哪个会被析构。
5. A a[5]; 在构造第3个元素时抛异常,前两个 A 会被析构。
6. 往函数传两个 A 对象 foo(A a1, A a2),要构造两个参数对象,如果后构造的那个 A::A() 抛异常,会导致已经构造好的
A 参数对象被析构。
7. 以 const reference 方式往函数传两个 A 对象 foo(const A& a1, const A& a2),然后
foo(1, 2.0) 会构造两个临时对象(假设 A 有合适的构造函数),如果后构造的那个 A::A() 抛异常,会导致已经构造好的 A 临时
对象被析构。

C++ 的异常比其他语言复杂得多,至少其他语言没有这么多自动析构的规则要记住。我估计还没有列完,只是凭记忆列了一些。

On Sep 11, 7:18 pm, up duan <fixo...@gmail.com> wrote:
> 给出这样的实例时,请给出编译器型号和版本。
>

> 2011/9/10 Yongwei Wu <wuyong...@gmail.com>
>
>
>
>
>
>
>
> > 2011/9/9 up duan <fixo...@gmail.com>:

jiang yu

unread,
Sep 11, 2011, 12:31:33 PM9/11/11
to pon...@googlegroups.com
My God, 不知wuyongwei现在什么状态,暴走了吗。

Yongwei Wu

unread,
Sep 11, 2011, 11:02:11 PM9/11/11
to pon...@googlegroups.com
呵呵,你想引诱我说一些会导致我被ban的话吗?:-)

不过,对于某些论点,我确实看得心头火起。有些东西,是可以简单测试一下就可以得到结论的。我做了20多年的C++开发,都会在发表我的论点前常常测试一下;所以我很不明白,为什么有人发表观点那么轻率。

讨论技术问题用花哨的文辞一般也不足取,你会更在意你的感觉而不是观点。讨论技术问题,需要的是就事论事。

回到Google对C++异常支持的问题,拿Xinyu说过的话,这多半是一个政治问题。坦率地说,我认为这是Google在这个问题上没有远见的表现。这可能也直接导致了Android对于C++的支持比起iOS差远了。

2011/9/12 jiang yu <yu.jia...@gmail.com>:

jiang yu

unread,
Sep 11, 2011, 11:17:15 PM9/11/11
to pon...@googlegroups.com
好吧,我承认对没有XXX,就没有XXX,就只能XXX的言论过敏了,就好像是没有XXX,就没有新中国一样。there always be another way.
C没有异常,靠个返回值,这么多年不一样好好的;C++是匹野马,像Google那样,对C++做裁剪,在工程上来讲是务实的态度。同样,我再吹毛求疵一下,这恐怕推出不这句话:
<坦率地说,我认为这是Google在这个问题上没有远见的表现。这可能直接导致了Android对于C++的支持比起iOS差远了。

Yongwei Wu

unread,
Sep 11, 2011, 11:43:27 PM9/11/11
to pon...@googlegroups.com
你去仔细看一下就会发现,没有异常,STL这样的泛型编程是不太可能的。在今天,没有STL和Boost的C++,还能算是C++吗?显然,我们对于C++的定义不同。

另外,在iOS开发中,我可以混合C++和Objective-C的代码,非常爽:

std::ostringstream oss;
oss << ...;
NSString *result = [NSString stringWithUTF8String:oss.str().c_str()];

换句话说,我可以直接在Objective-C代码中使用C++的接口。在Android中,C++异常都只是近来才支持的,不要说JNI的麻烦事了。

2011/9/12 jiang yu <yu.jia...@gmail.com>:


> 好吧,我承认对没有XXX,就没有XXX,就只能XXX的言论过敏了,就好像是没有XXX,就没有新中国一样。there always be another
> way.
> C没有异常,靠个返回值,这么多年不一样好好的;C++是匹野马,像Google那样,对C++做裁剪,在工程上来讲是务实的态度。同样,我再吹毛求疵一下,这恐怕推出不这句话:
> <坦率地说,我认为这是Google在这个问题上没有远见的表现。这可能直接导致了Android对于C++的支持比起iOS差远了。
>
> 在 2011年9月12日 上午11:02,Yongwei Wu <wuyo...@gmail.com>写道:
>>
>> 呵呵,你想引诱我说一些会导致我被ban的话吗?:-)
>>
>>
>> 不过,对于某些论点,我确实看得心头火起。有些东西,是可以简单测试一下就可以得到结论的。我做了20多年的C++开发,都会在发表我的论点前常常测试一下;所以我很不明白,为什么有人发表观点那么轻率。
>>
>> 讨论技术问题用花哨的文辞一般也不足取,你会更在意你的感觉而不是观点。讨论技术问题,需要的是就事论事。
>>
>>
>> 回到Google对C++异常支持的问题,拿Xinyu说过的话,这多半是一个政治问题。坦率地说,我认为这是Google在这个问题上没有远见的表现。这可能也直接导致了Android对于C++的支持比起iOS差远了。
>>
>> 2011/9/12 jiang yu <yu.jia...@gmail.com>:
>> > My God, 不知wuyongwei现在什么状态,暴走了吗。
>> >
>> > 在 2011年9月11日 下午9:59,Yongwei Wu <wuyo...@gmail.com>写道:
>> >>
>> >>
>> >>
>> >> 没有异常,你就只能利用函数的返回值来表示错误,也就意味着你几乎不可能让一个函数返回一个对象,也就意味着STL和Boost变得几乎不可能。也就是说,C++就根本不是今天的C++了。
>> >>
>> >>
>> >>
>> >> 混合语言编程不是问题,因为异常本来就该是C++语言内部使用的。你会想让Java的异常抛到C++里去吗?动态库也类似,这是一个设计问题。至于析构时崩溃就更扯了,任何C++的书都会告诉你析构时不允许抛任何异常。
>> >>
>> >>
>> >>
>> >> 如果你说C++没有STL和Boost也挺好,我只能说,这不是C++社区的主流观念。建议你考虑Objective-C,简单,没有异常,目前流行程度也不错,只是结果程序多半只能跑在苹果的平台上。

--

yuan zhu

unread,
Sep 12, 2011, 5:43:36 AM9/12/11
to pon...@googlegroups.com
高度依赖异常的stl 组件,一般只怕很少有人用哟。

boost里面使用异常的往往也是在那些自定义的泛型cast 中才用吧,如boost::any 与boost::variant 的boost::any_cast和boost::get之流的,(自定义转换强制要求了返回值类型)。其他常用组件应该很少使用异常吧。

另外混合语言编程里面,兄弟试过c++异常穿越obj-c栈传播的结果吗,啧啧。错误反馈回来的dump文件,不仅实际错误几乎完全远离现场,且传播异常的时候又导致obj-c的一些代码错误( 因为obj-c不捕捉c++异常,眼睁睁的看着c++异常路过),郁闷死人了。

推荐兄弟开发不要光顾着爽。开发是个团队活,谨慎点好,不要用一些高级特性让自己爽了,让同事们郁闷。尤其是混合语言编程。


2011/9/12 Yongwei Wu <wuyo...@gmail.com>

jinhu wang

unread,
Sep 12, 2011, 5:52:47 AM9/12/11
to pon...@googlegroups.com
没看明白,几个小问题。
异常跟泛型编程怎么搅和到一块的?
stl也就罢了,boost怎么又成了c++的必然条件?最新c++标准里有boost吗?

qiaojie

unread,
Sep 12, 2011, 8:52:34 AM9/12/11
to pon...@googlegroups.com
混合编程的异常处理,需要在调用边界的地方将异常转换成错误返回值,这个属于基本常识了。同样STL、Boost之类的库没有异常就不能用了,这种情况也比较罕见。
我对C++异常的看法保持中立态度,异常使用得当会使代码变得干净简洁,但也不是非用不可,在实际工程中对C++特性做一些裁剪也是无可厚非的。怕就怕的是由于对异常一知半解、错误使用出了问题,然后就把异常贬低的一文不值;或者是C++的死忠,不把C++特性都用上就感觉不爽,追求语言特性甚于项目本身,这些都不可取。


2011/9/12 yuan zhu <zy49...@gmail.com>

Yongwei Wu

unread,
Sep 12, 2011, 9:15:37 AM9/12/11
to pon...@googlegroups.com
连最普通的vector,都是依赖于异常的。内存不足时会抛bad_alloc;另外,由于vector里放的对象可能会抛异常,唯一可能的通用处理方法就是使用异常。

已经说过了,异常不应该穿越语言边界。几乎所有此类代码可以看作是设计错误。我们的项目中,一般对外只暴露C的接口,错误接口必须良好定义,异常也是不允许跑到模块外的。

2011/9/12 yuan zhu <zy49...@gmail.com>:

Zhangming Niu

unread,
Sep 12, 2011, 9:21:00 AM9/12/11
to pon...@googlegroups.com
throw exceptions are not good and  hard to be traced sometimes.

2011/9/12 Yongwei Wu <wuyo...@gmail.com>



--
--------------------------------------------------------------------

Zhangming Niu

I.T consultant at Ericsson AB, Sweden



qiaojie

unread,
Sep 12, 2011, 9:39:13 AM9/12/11
to pon...@googlegroups.com
如果明确不用异常的话,可以把new的行为定义成no throw,当然也不允许让vector里放的对象抛异常,另外可以在编译器里关掉异常,STL的实现里也会区分有异常和没异常两种情况分别做不同处理。


2011/9/12 Yongwei Wu <wuyo...@gmail.com>

Yongwei Wu

unread,
Sep 12, 2011, 10:05:40 AM9/12/11
to pon...@googlegroups.com
那你怎么表示初始化错误?比如:

vector<Object> v(100);

还有很多情况,比如复制构造和赋值。

Java和Objective-C没这样的问题,因为所有的对象都是堆上构造,按指针(引用)传递。在允许按值传递的C++里,不用异常,泛型基本无解。

2011/9/12 qiaojie <qia...@gmail.com>:

Yongwei Wu

unread,
Sep 12, 2011, 10:06:20 AM9/12/11
to pon...@googlegroups.com
Boost是C++标准的实验田。TR1里的新加库,基本上都在Boost试验过了的。

2011/9/12 jinhu wang <wangji...@gmail.com>:

Yongwei Wu

unread,
Sep 12, 2011, 10:07:33 AM9/12/11
to pon...@googlegroups.com

Yongwei Wu

unread,
Sep 12, 2011, 10:16:25 AM9/12/11
to pon...@googlegroups.com
我觉得误用异常的可能性更大些。语言是不能靠几句公司标准规范的,一定要用评审来告诉新手该怎样写程序。

“不把C++特性都用上就感觉不爽”,有吗?:-)
事实上,很少有人能掌握所有的C++特性。该用的时候还是应该用。活到老,学到老。异常是90年代中期加入的,我还没见过一位C++大师说异常不该加入:Stroustrup,
Alexandrescu, Meyers, Koenig, Sutter, ……

2011/9/12 qiaojie <qia...@gmail.com>:


> 混合编程的异常处理,需要在调用边界的地方将异常转换成错误返回值,这个属于基本常识了。同样STL、Boost之类的库没有异常就不能用了,这种情况也比较罕见。
> 我对C++异常的看法保持中立态度,异常使用得当会使代码变得干净简洁,但也不是非用不可,在实际工程中对C++特性做一些裁剪也是无可厚非的。怕就怕的是由于对异常一知半解、错误使用出了问题,然后就把异常贬低的一文不值;或者是C++的死忠,不把C++特性都用上就感觉不爽,追求语言特性甚于项目本身,这些都不可取。

--

Yongwei Wu

unread,
Sep 12, 2011, 10:27:05 AM9/12/11
to pon...@googlegroups.com
http://www.codeproject.com/KB/cpp/cppexceptionsproetcontra.aspx

结论部分写道:

There is no simple answer to the "exceptions or error codes" question.
The decision needs to be made based on a specific situation that a
development team faces. Some rough guidelines may be:

* If you have a good development process and code standards that are
actually being followed, if you are writing modern-style C++ code
that relies on RAII to clean up resources for you, if your code base
is modular, using exceptions may be a good idea.
* If you are working with code that was not written with exception
safety in mind, if you feel there is a lack of discipline in your
development team, or if you are developing hard real-time systems,
you should probably not use exceptions.

2011/9/12 qiaojie <qia...@gmail.com>:
> 如果明确不用异常的话,可以把new的行为定义成no throw,当然也不允许让
> vector里放的对象抛异常,另外可以在编译器里关掉异常,STL的实现里也会
> 区分有异常和没异常两种情况分别做不同处理。
>

> 2011/9/12 Yongwei Wu <wuyo...@gmail.com>
>>
>>
>> 连最普通的vector,都是依赖于异常的。内存不足时会抛bad_alloc;另外,
>> 由于vector里放的对象可能会抛异常,唯一可能的通用处理方法就是使用异
>> 常。
>>
>> 已经说过了,异常不应该穿越语言边界。几乎所有此类代码可以看作是设计
>> 错误。我们的项目中,一般对外只暴露C的接口,错误接口必须良好定义,异
>> 常也是不允许跑到模块外的。
>>
>> 2011/9/12 yuan zhu <zy49...@gmail.com>:
>> > 高度依赖异常的stl 组件,一般只怕很少有人用哟。
>> > boost里面使用异常的往往也是在那些自定义的泛型cast 中才用吧,如boost::any 与boost::variant
>> > 的boost::any_cast和boost::get之流的,(自定义转换强制要求了返回值类型)。其他常用组件应该很少使用异常吧。
>> >
>> > 另外混合语言编程里面,兄弟试过c++异常穿越obj-c栈传播的结果吗,啧啧。错误反馈回来的dump文件,不仅实际错误几乎完全远离现场,且传播异常的时候又导致obj-c的一些代码错误(
>> > 因为obj-c不捕捉c++异常,眼睁睁的看着c++异常路过),郁闷死人了。
>> > 推荐兄弟开发不要光顾着爽。开发是个团队活,谨慎点好,不要用一些高级特性让自己爽了,让同事们郁闷。尤其是混合语言编程。

--

qiaojie

unread,
Sep 12, 2011, 10:36:55 AM9/12/11
to pon...@googlegroups.com
确实,这个是C++的一个缺陷,在构造函数里不能返回错误值,只能用异常。而解法前面也提到了,用两段式构造来绕过去。如果你往容器里放的是值对象,那么多半是简单的POD对象,没理由抛异常,如果是复杂的需要抛异常的对象,那么应该考虑放引用,那就可以用两段式构造来检查错误值了。但是由于STL容器本身没有采用2段式构造,构造一个大的vector时内存不够,那就没有办法了。这种情况基本上仅限于内存不足的条件下,从实用主义的角度出发,我们在编程时总是假设内存是足够的,而不会处处小心的去处理内存不足的情况(那样写程序太辛苦了),在内存不足的情况下,即使有异常机制也无法让程序健壮起来(处理异常的时候也需要内存的)。所以对于内存不足的异常,一般可以忽略不计。操作系统在低内存时会警告用户,保证程序在运行期内总有适量的可分配内存,如果你的程序需要分配大内存,可以事先判断一下是否有足够的内存。





2011/9/12 Yongwei Wu <wuyo...@gmail.com>

qiaojie

unread,
Sep 12, 2011, 10:45:20 AM9/12/11
to pon...@googlegroups.com
确实,这个是C++的一个缺陷,在构造函数里不能返回错误值,只能用异常。而解法前面也提到了,用两段式构造来绕过去。如果你往容器里放的是值对象,那么多半是简单的POD对象,没理由抛异常,如果是复杂的需要抛异常的对象,那么应该考虑放引用,那就可以用两段式构造来检查错误值了。但是由于STL容器本身没有采用2段式构造,构造一个大的vector时内存不够,那就没有办法了。不过好在这种情况基本上仅限于内存不足,从实用主义的角度出发,我们在编程时总是假设内存是足够的,而不会处处小心的去处理内存不足的情况(那样写程序太辛苦了),在内存不足的情况下,即使有异常机制也无法让程序健壮起来(处理异常的时候也需要内存的)。所以对于内存不足的异常,一般可以选择忽略。操作系统在低内存时会警告用户,保证程序在运行期内总有适量的可分配内存,如果你的程序需要分配大内存,可以预选判断一下是否有足够的内存。





2011/9/12 Yongwei Wu <wuyo...@gmail.com>

Zhangming Niu

unread,
Sep 12, 2011, 10:50:09 AM9/12/11
to pon...@googlegroups.com
most computers/OSs support virtual memory, backed by disk space. So memory will be quite enough if you allocate space from heap(as long as your hard disk space available).
stack is around 2MB in modern computers but could also be configured


2011/9/12 qiaojie <qia...@gmail.com>

Yongwei Wu

unread,
Sep 12, 2011, 11:00:06 AM9/12/11
to pon...@googlegroups.com
问题是,两段式构造才是不标准的,无法跟容器配合。引入的未完成对象的概念,在我看来也是一种灾难。

2011/9/12 qiaojie <qia...@gmail.com>:

bronco

unread,
Sep 12, 2011, 10:21:15 PM9/12/11
to pon...@googlegroups.com
额,偶用9年C++,极少抛异常,顶多只是cacth别人的异常,然后变成错误码再走。

前面qiaojie说道:
1:”混合编程的异常处理,需要在调用边界的地方将异常转换成错误返回值,这个属于基本常识了“;
2:"确实,这个是C++的一个缺陷,在构造函数里不能返回错误值,只能用异常",这里的避免方法多了去了。但是我觉得:先检查下你的设计吧,我肯定有不合理的地方,才会导致在构造的时候要抛出异常,谁贴一个case给我挑战一下?


C++规则确实太多,自我形成一些规则将C++裁减掉吧。
象:template<class T> 之类的东西,偶统一template<typename T>,
某日一新人问我两者区别,偶毫不犹豫的让他没理由的忘记template<class T>这种写法

jinhu wang

unread,
Sep 12, 2011, 11:12:36 PM9/12/11
to pon...@googlegroups.com
哪个领域的c++开发?领域不同,对c++的特性需求也不同。
就单拿通信领域来说,横向大局观的有下面几种类型的软件:
网管,
核心网,
接入网,
操作系统,
工具软件;
纵向的看又有:
配置管理层面,
协议控制层面,
数据层面;
每个领域每个层面对软件的需求侧重点各不相同。硬件的基础配置也不同。假设以上所有领域所有层面都用c++,每个层面选取c++特性也会有相应的差异。

Yongwei Wu

unread,
Sep 13, 2011, 4:50:55 AM9/13/11
to pon...@googlegroups.com
给你一个case。我们内部使用UTF-8,但和Windows的系统接口打交道需要使用
UTF-16。在使用临时对象和异常时,我的代码如下:

try {
// Do something
SomeWindowsFunction(Utf8ToWideString(my_string));
// Do other things
} catch (ConversionError& e) {
// Error processing
}

如果不使用异常:

// Do something
wstring temp_wstr;
if (!ConvertUtf8ToWideString(my_string, temp_wstr)) {
// Error processing
}
SomeWindowsFunction(temp_wstr);
// Do other things

上面的对象还是使用异常了,因为string可能会抛异常。一般还是可以工作的,
但更严格的代码应当类似这样:

// Do something
size_t buffer_len = strlen(my_string) + 1;
wchar_t* buffer = (wchar_t*)alloca(buffer_len * sizeof(wchar_t));
if (!ConvertUtf8ToWideString(my_string, buffer, buffer_len)) {
// Error processing
}
SomeWindowsFunction(buffer);
// Do other things

在转换较多的情况下,使用异常的优势是非常明显的。否则,大量错误判断代码
会非常严重地干扰正常的代码阅读。

除了使用临时对象的情况,容器和模板也依赖于异常。除非你能够让所有的人使
用你的二段式初始化的标准,改造或者不使用STL,并且在代码中不使用复制构
造和赋值表达式。

2011/9/13 bronco <renfe...@gmail.com>:

--

Xinyu LIU

unread,
Sep 13, 2011, 5:00:05 AM9/13/11
to pon...@googlegroups.com
Hi,

本来没有想到对RAII和exception会有这么大的分歧。直接看下面的代码吧。
我们希望那个做如下的测试:

void test_raii(){
  for(int i=0; i<10000; ++i){
    reset_all();
    raii_proc();
    assert_all();
  }
  std::cout<<"10000 test passed\n";
}

运行N遍测试以保证所有可能产生错误或者异常的操作中,资源全部被安全释放。

其中可能出问题的raii_proc定义如下:

void raii_proc(){
  try{
    File f("foo");
    Socket sock("foo.bar.com");
    DBWrapper db(sock.s, "user");
    f.put(db.get(1));
  }catch(std::string& e){
    //std::cout<<e<<"\n";
  }
}

求是我前阵说的,打开文件,打开socket,然后打开远程db,然后访问之。

在raii_proc之前,我们把一组invariant标志reset掉:
//for invariant testing
static int file_opened;
static int socket_opened;
static int db_opened;

void reset_all(){
  file_opened = 0;
  socket_opened = 0;
  db_opened = 0;
}

测试结束后,我们检查他们是否成功都恢复到原始状态了:
void assert_all(){
  assert(!file_opened);
  assert(!socket_opened);
  assert(!db_opened);
}

然后我们定义一组传统方式,用error code处理的的file和socket API,为了测试,我们让他们有一半(50%)的机会出错。
typedef int handle;

// unstable file operation
// traditional C file like error code
handle ufopen(const char* /*filename*/){
  if(rand()%2)
    return 0;
  file_opened = 1;
  return 1;
}

void ufclose(handle){ file_opened = 0; }

int fput(handle, const char* /*data*/){
  return rand()%2 ? 0: 1;
}

然后是一组容易出错的socket API:
// unstable socket
// traditional C like error code
handle sconnect(const char* /*addr*/){
  if(rand()%2)
    return 0;
  socket_opened = 1;
  return 1;
}

void sclose(handle){ socket_opened = 0; }

int sget(handle){
  return rand()%2 ? 0: 1;
}

为了说明混合多个库遇到的问题,我们再给一组已经使用异常的DB的API
// unstable data base
// use exception to indicate issues
struct Database{
  enum { OPEN_ERR, READ_ERR };

  Database(){}

  Database(handle socket, const char* name){
    open(socket, name);
  }

  void open(handle /*socket*/, const char* /*name*/){
    if(rand()%2)
      throw std::string("db open err!");
    db_opened = 1;
  }

  char* get(int /*key*/){
    if(rand()%2)
      throw std::string("db read err!");
    return 0;
  }

  void close(){ db_opened = 0; }
};

首先,我们看比较麻烦的做法,给每个资源都写一个RAII的Wrapper。首先是File的:
struct File{
  handle f;
  File(const char* name){
    if((f=ufopen(name))==0)
      throw std::string("file open err!");
  }

  void put(const char* data){
    if(!fput(f, data))
      throw std::string("file write err!");
  }

  ~File(){ ufclose(f); }
};

然后是Socket的:
struct Socket{
  handle s;
  Socket(const char* addr){
    if((s=sconnect(addr))==0)
      throw std::string("sock open err!");
  }

  ~Socket(){ sclose(s); }
};

最后是Database的:
struct DBWrapper{
  Database* db;
  DBWrapper(handle socket, const char* name):db(0){
    db = new Database(socket, name);
  }
 
  char* get(int key){
    if(db)
      return db->get(key);
    throw std::string("db read err!");
  }

  ~DBWrapper(){
    if(db)
      db->close();
  }
};

下面就可以测试了。
我的结果:

liuuuxin@wl025474 ~/temp/cpp
# g++ raii.cpp -o raii

liuuuxin@wl025474 ~/temp/cpp
# ./raii
10000 test passed

如果觉得这个方法麻烦,可以用抽象File, Socket和DB的共同点,搞出个自己的RAII Wrapper。当然也可以使用boost(如果你的组织允许的话),注意到shared_ptr有如下的ctor:
template <class Y,class D> shared_ptr(Y* p,D d);

其实仅仅就这个问题,希望使用scoped_ptr的估计是第一感觉,但是scoped_ptr没有这个重载版本,所以无法把release函数传入。

最后,不知道有没有人看过C++ coding standard 101 rules, guidelines and best practices?其中第13章讨论这个topic

2011/9/13 jinhu wang <wangji...@gmail.com>



--
Larry, LIU Xinyu
https://sites.google.com/site/algoxy/
https://github.com/liuxinyu95/AlgoXY

Xinyu LIU

unread,
Sep 13, 2011, 5:11:47 AM9/13/11
to pon...@googlegroups.com
Hi,

对了,好多人都提到两阶段构造。要知道这个在Symbian C++下强制要求的。
两阶段构造配合清除栈使用(注:我不遵守Nokia的Code Convention中的缩进了):

class CMyClass: public CObject{
public:
  CMyClass();
  void ConstructL(type param);
  static CMyClass* NewL(type param);
  ~CMyClass();
};

CMyClass::CMyClass(){}

void ConstructL(type param){
  // do things may leave
}

CMyClass* CMyClass::NewL(type param){
  CMyClass* self = new (ELeave) CMyClass();
  CleanupStack.PushL(self);
  self->ConstructL(param);
  CleanupStack.Pop(); //self
  return self;
}

然后client code如下:
void FooL(){
  CMyClass obj=CMyClass::NewL(param);
  obj->funcL(...);
}

无数写Symbian/S60程序的工程师烦死这个了。然后我们问,为啥不用exception。
Nokia的官方解释是,这是个历史问题,Symbian发明的时候,C++还不支持exception。
然而在Symbian开源后,我们看到Symbian 9的源代码中,在wins里,直接使用了C++的异常,里面赫然是try-catch。
在arm里,里面仍然使用了longjmp和setjmp。

2011/9/13 Xinyu LIU <liuxi...@gmail.com>

bronco

unread,
Sep 13, 2011, 11:12:31 AM9/13/11
to pon...@googlegroups.com
这个case的设计没有挑战性啊,你自己都把不用类得构造都写出来了。
另外:你的问题:大量错误代码干扰代码阅读?

1:将你的错误判断逻辑lwrapper成funcA;
2:将你的错误处理逻辑与funcA再wrapper成funcB;
3:如果你的错误处理逻辑非常多导致funcB不好Wrapper出来,或者Wrapper出来用处不大的话,
那么你对应的try-catch面临同样的问题;

Yongwei Wu

unread,
Sep 13, 2011, 9:31:01 PM9/13/11
to pon...@googlegroups.com
从根本上说,图灵机都是等价的。不存在一种表达式,在一种图灵完全的语言中
完全无法表达。但要知道,程序员的生产力是和写的代码行数基本成正比的。这
就是为什么使用高级语言生产力高。C++提供了高生产力、性能损失也很低的表
达方式,为什么不用?

你的要求挑战的原话是:"我肯定有不合理的地方,才会导致在构造的时候要抛
出异常"。就请问一下,我的Utf8ToWideString,构造时遇到非法UTF-8序列抛
异常,有什么不合理的地方?

我这儿没有表达出来的另外一点是,我在输入由自己决定的时候,在大部分函数
里可以完全不处理这个异常,只在较外层的地方catch一下,当前的操作失败。
使用返回值,你得在函数的每一层里进行处理。麻烦,易出错。

除了以上的一般异常好处外,再强调一遍,要使用C++的函数按值返回对象、临
时对象、泛型编程,异常基本不可或缺。你当然可以选择不用这些特性,但说别
人使用这些特性"不合理"则是另外一回事了。

2011/9/13 bronco <renfe...@gmail.com>:

bronco

unread,
Sep 13, 2011, 10:34:02 PM9/13/11
to pon...@googlegroups.com
你说的很对,我说的话不够严格,咱应该对“合理”二字加上一个定义,你我对"合理"的理解并不相同,才会有分歧;
你认为在语法上是合理的即是合理,我也无法反驳;就像在构造函数中,啥也不干只抛出个异常,谁也找不出问题,他也是“合理”的。

yuan zhu

unread,
Sep 15, 2011, 3:50:54 AM9/15/11
to pon...@googlegroups.com
兄弟我说的是上文那个objective-c混合c++编程那种,和你说的分模块调接口那种玩意,完全是2回事。你去看那个人的stringstream的例子吧。

这个世界的设计如果都想你想的那样,永远不变化,瀑布式开发,那当然就爽得很了。

Yongwei Wu

unread,
Sep 15, 2011, 12:04:42 PM9/15/11
to pon...@googlegroups.com
那是我的例子。我可没有让C++异常穿过Objective-C。那段代码里,对oss的操
作全部是C++的代码,从C++的对象里得到结果,只在最后,把结果转成
Objective-C的字符串,传回到Objective-C的代码里去。C++的异常都在C++的代
码里处理完了。

里面唯一不够严格的是没有处理可能的bad_alloc。不过,在iPhone环境里,程
序在收到bad_alloc之前似乎就已经被OS杀掉了,所以,也不怎么必要。

我可不用瀑布式开发。在非瀑布式的开发环境里,模块间的接口仍然是需要清楚
定义的(变化需要实现者和使用者之间进行协调)。不定义好,肯定会导致混乱
的编程结果。

2011/9/15 yuan zhu <zy49...@gmail.com>:

jinhu wang

unread,
Sep 15, 2011, 10:40:51 PM9/15/11
to pon...@googlegroups.com
关于语言的细节现在的我有以下观点:)
编程语言类比于人类语言。很多时候我们并不在乎你使用了多少华丽的词汇,我们更在意你讲出的故事是否精彩。
关于混合编程,很像在外企里用chinglish去描述你做的事情。。。

Xinyu LIU

unread,
Sep 16, 2011, 4:37:13 AM9/16/11
to pon...@googlegroups.com
Hi,

我再次重申乔姆斯基(Chomsky)的观点:语言可以影响人的思维,举个例子:
  - 日语的结构是: 主语+宾语+谓语
我们可以看到这多大程度影响了日本人的思维(请切勿联想发挥到其他层面,不然这个thread就危险了)。

PS: 如果按照被引用来排名,MIT数据显示,乔姆斯基是科学界影响最大的人。对人类影响最大的的top 10中,唯一活着的人。
另外的9位包括柏拉图,亚里士多德,西塞罗,黑格尔,莎士比亚等等,绝大多数是哲学家。

2011/9/16 jinhu wang <wangji...@gmail.com>

jinhu wang

unread,
Sep 16, 2011, 11:23:42 PM9/16/11
to pon...@googlegroups.com
我再次重申我的观点:)不管你知道“回”字有多少种写法,写出有价值的代码就值得尊重。
PS:我一直以为昌平是宇宙的中心,因为这里汇集了BJ大部分的程序员。。。不知道你提的那10个人在不在这边住。
c++.JPG

居振梁

unread,
Sep 16, 2011, 11:29:19 PM9/16/11
to pon...@googlegroups.com
我觉得很奇怪,你干嘛非要较这个真。
IT 技术经理里,自己带着个“研发团队”,却对“研究”有着不可节制的偏见,这种人不少见吧?

2011/9/16 Xinyu LIU <liuxi...@gmail.com>

Hi,

我再次重申乔姆斯基(Chomsky)的观点:语言可以影响人的思维,举个例子:
  - 日语的结构是: 主语+宾语+谓语
我们可以看到这多大程度影响了日本人的思维(请切勿联想发挥到其他层面,不然这个thread就危险了)。

PS: 如果按照被引用来排名,MIT数据显示,乔姆斯基是科学界影响最大的人。对人类影响最大的的top 10中,唯一活着的人。
另外的9位包括柏拉图,亚里士多德,西塞罗,黑格尔,莎士比亚等等,绝大多数是哲学家。

--
WarGrey
Field Title: Independent Scientist
Life Role: x-spirit-hackartist
Website: http://xspirithack.org
Sport: Ninjutsu Parkour

yuan zhu

unread,
Sep 17, 2011, 1:42:39 AM9/17/11
to pon...@googlegroups.com
不想和这个哥们儿谈了,前后矛盾又忽左忽右的。

自己都知道代码里随时可能有std::bad_alloc这样的稍微不注意就会忽略掉的因子,还说C++异常不会穿过Objective-C栈;然后前面又说stl依赖这些异常,这些异常很重要,c++不能没有异常不然stl工作不下去了;然后再说这些异常咱们可以不当回事,直接扔给系统管吧,set_new_handler都省去了......

看到这么洒脱的言论,我真为我项目组里的人通宵分析用户反馈的dump感觉不值。另外我又一次见到了stringstream这样的大神代码,实在值得做个标记留念一下。

另外怎么老有人拿做库的方法和做app的方法混淆呢?

2011/9/16 Yongwei Wu <wuyo...@gmail.com>

Yongwei Wu

unread,
Sep 18, 2011, 10:24:41 PM9/18/11
to pon...@googlegroups.com
2011/9/17 yuan zhu <zy49...@gmail.com>:
> 不想和这个哥们儿谈了,前后矛盾又忽左忽右的。

你有不回复的自由:-)。希望继续讨论的话,请就事论事。

> 自己都知道代码里随时可能有std::bad_alloc这样的稍微不注意就会忽略掉的
> 因子,还说C++异常不会穿过Objective-C栈;

iPhone里内存不足的话,程序会被系统杀掉,catch也没什么用。在我的情况
里,那段是核心代码(关键任务在C++里完成),即使能catch住,也做不了什么
事情了。

> 然后前面又说stl依赖这些异常,这些异常很重要,c++不能没有异常不然stl
> 工作不下去了;

当然,不然你写一个不依赖于异常的STL出来试试!

可以想象,下面的语句都是非法的:

string s1("Hello");
string s2 = s1;
s1 = "World";
s2 += ' ';
s2 += s1;

你确认,你想要的是这样的代码:

string s1;
if (!s1.init("Hello"))
goto Bad_alloc_exit;
string s2;
if (!s2.init(s1))
goto Bad_alloc_exit;
...

> 然后再说这些异常咱们可以不当回事,直接扔给系统管吧,

仅限于iPhone下bad_alloc的情况,是这样。

> set_new_handler都省去了......

这本来就不是必需的。缺省行为是bad_alloc,我不觉得有什么问题。

> 看到这么洒脱的言论,我真为我项目组里的人通宵分析用户反馈的dump感觉不
> 值。另外我又一次见到了stringstream这样的大神代码,实在值得做个标记留
> 念一下。

为什么不能用stringstream呢?你推荐用什么?希望不是s*printf吧。

> 另外怎么老有人拿做库的方法和做app的方法混淆呢?

如果你是指我写的"模块间的接口仍然是需要清楚定义的",我不认为有任何混
淆的地方。你们的应用程序里也应该分模块、有清晰的模块界限吧。

qiaojie

unread,
Sep 19, 2011, 1:03:25 AM9/19/11
to pon...@googlegroups.com
好奇怪啊,前面说bad_alloc不用处理,后面又说不处理bad_alloc是非法的。
到底要不要处理bad_alloc?bad_alloc是不是可以关掉?STL是不是可以不依赖bad_alloc?能不能给个准话?


2011/9/19 Yongwei Wu <wuyo...@gmail.com>

yuan zhu

unread,
Sep 19, 2011, 2:40:04 AM9/19/11
to pon...@googlegroups.com
默认的STL 容器的 allocator依赖,但是你可以设法set_new_handler避开 不在同一个栈帧上处理分配问题给程序结构带来的 不便。

2011/9/19 qiaojie <qia...@gmail.com>

Yongwei Wu

unread,
Sep 19, 2011, 10:22:07 AM9/19/11
to pon...@googlegroups.com
2011/9/19 qiaojie <qia...@gmail.com>:
> 好奇怪啊,前面说bad_alloc不用处理,后面又说不处理bad_alloc是非法的。

我可没说不处理是非法的。我说的是如果C++没有异常的话,合法的使用STL的代
码会长成那个样子。

> 到底要不要处理bad_alloc?bad_alloc是不是可以关掉?

拿句套话:It depends。在不同的应用场景下,处理各不相同。

> STL是不是可以不依赖bad_alloc?能不能给个准话?

作为一个通用的模板库,我认为不可以。

--

jinhu wang

unread,
Sep 24, 2011, 6:09:17 AM9/24/11
to pon...@googlegroups.com
请教大家两个问题:
  1. 一般用异常做什么?
  2. 内存失败抛出异常,有多少同学真正在程序里捕获到过内存失败异常。

Shuo Chen

unread,
Sep 24, 2011, 6:34:34 AM9/24/11
to TopLanguage
http://www.newsmth.net/bbstcon.php?board=CPlusPlus&gid=332108

On Sep 24, 6:09 pm, jinhu wang <wangjinhu...@gmail.com> wrote:
> 请教大家两个问题:
>
> 1. 一般用异常做什么?
> 2. 内存失败抛出异常,有多少同学真正在程序里捕获到过内存失败异常。
>
> 在 2011年9月19日 下午10:22,Yongwei Wu <wuyong...@gmail.com>写道:
>
>
>
>
>
>
>
> > 2011/9/19 qiaojie <qiao...@gmail.com>:


> > > 好奇怪啊,前面说bad_alloc不用处理,后面又说不处理bad_alloc是非法的。
>
> > 我可没说不处理是非法的。我说的是如果C++没有异常的话,合法的使用STL的代
> > 码会长成那个样子。
>
> > > 到底要不要处理bad_alloc?bad_alloc是不是可以关掉?
>
> > 拿句套话:It depends。在不同的应用场景下,处理各不相同。
>
> > > STL是不是可以不依赖bad_alloc?能不能给个准话?
>
> > 作为一个通用的模板库,我认为不可以。
>

> > > 2011/9/19 Yongwei Wu <wuyong...@gmail.com>
>
> > >> 2011/9/17 yuan zhu <zy498...@gmail.com>:

> > >> > 2011/9/16 Yongwei Wu <wuyong...@gmail.com>


>
> > >> >> 那是我的例子。我可没有让C++异常穿过Objective-C。那段代码里,对oss的操
> > >> >> 作全部是C++的代码,从C++的对象里得到结果,只在最后,把结果转成
> > >> >> Objective-C的字符串,传回到Objective-C的代码里去。C++的异常都在C++的代
> > >> >> 码里处理完了。
>
> > >> >> 里面唯一不够严格的是没有处理可能的bad_alloc。不过,在iPhone环境里,程
> > >> >> 序在收到bad_alloc之前似乎就已经被OS杀掉了,所以,也不怎么必要。
>
> > >> >> 我可不用瀑布式开发。在非瀑布式的开发环境里,模块间的接口仍然是需要清楚
> > >> >> 定义的(变化需要实现者和使用者之间进行协调)。不定义好,肯定会导致混乱
> > >> >> 的编程结果。
>

> > >> >> 2011/9/15 yuan zhu <zy498...@gmail.com>:
>
> > 兄弟我说的是上文那个objective-c混合c++编程那种,和你说的分模块调接口那种玩意,完全是2回事。你去看那个人的stringstream的例子 吧。
>
> > >> >> > 这个世界的设计如果都想你想的那样,永远不变化,瀑布式开发,那当然就爽得很了。


>
> > >> >> > On 9/12/11, Yongwei Wu <wuyong...@gmail.com> wrote:
>
> > 连最普通的vector,都是依赖于异常的。内存不足时会抛bad_alloc;另外,由于vector里放的对象可能会抛异常,唯一可能的通用处理方法就是使 用异常。
>

> > 已经说过了,异常不应该穿越语言边界。几乎所有此类代码可以看作是设计错误。我们的项目中,一般对外只暴露C的接口,错误接口必须良好定义,异常也是不允许跑到 模块外的。
>
> > >> >> >> 2011/9/12 yuan zhu <zy498...@gmail.com>:

Yongwei Wu

unread,
Sep 24, 2011, 6:45:30 AM9/24/11
to pon...@googlegroups.com
2011/9/24 jinhu wang <wangji...@gmail.com>:
> 一般用异常做什么?

我想这个有很多资料可以查吧。

随便说两句。一般用异常描述不容易发生、正常流程以外的错误。带来的主要好处是我可以在好几层以上的函数里统一处理,不用层层检查。

另外,如前面讨论过,如果要让构造函数、某些操作符返回错误,异常是唯一的方式。

> 内存失败抛出异常,有多少同学真正在程序里捕获到过内存失败异常。

正常程序里不多。但仍需考虑到:

* 我见过物理内存充足的情况下内存分配有暂时的失败(Windows)
* 有些内存分配操作是用户(包括文件、网络等)输入引起的,程序需要判断内存分配是否失败,以免用户构造攻击
* 内存不足时,仍应当尽量保存用户数据

Jeff Chen

unread,
Sep 24, 2011, 11:14:36 AM9/24/11
to pon...@googlegroups.com
最近遇到一个BUG,已经调查2周了,还没有解决:

在Vxworks上(SMP mode),创建一个RTP(Real time process).

在4核的情况下完全正常
在8核的情况下,创建RTP失败.

通过对系统库函数的debug,目前发现在8核情况下某个内存里改掉值了(4个byte)。

是C语言的. 

下周想设断点,但是很麻烦.
有没有人有相关经验的?
--
My Blog:http://jeffchen.cn

oliver yang

unread,
Sep 24, 2011, 11:19:33 AM9/24/11
to pon...@googlegroups.com
你的debugger支持不支持设置memory watch point?支持的话就可以看谁改了内存,例如某些野指针的bug.

--
Cheers,

Oliver Yang

Twitter: http://twitter.com/yangoliver
Blog:    http://blog.csdn.net/yayong
--------------------------------------------------------------------
An OpenSolaris Developer

Zhangming Niu

unread,
Sep 24, 2011, 4:17:44 PM9/24/11
to pon...@googlegroups.com
Jeff Chen:

你cpu是什么的,是不是hyper threading的。



2011/9/24 oliver yang <yango...@gmail.com>



--
--------------------------------------------------------------------
Best Regards,

Zhangming Niu




Jeff Chen

unread,
Sep 25, 2011, 1:22:39 AM9/25/11
to pon...@googlegroups.com
我们用的是飞思卡尔的P4080板子

Jeff Chen

unread,
Sep 25, 2011, 1:23:41 AM9/25/11
to pon...@googlegroups.com
需要用仿真器才能做到这个,比较麻烦的,下周常识跟踪这个memory。

jinhu wang

unread,
Sep 25, 2011, 9:57:56 PM9/25/11
to pon...@googlegroups.com
嗯,过年前后,我们曾经有个项目是基于vxworks的smp mod做路由器的。现在项目cancel了,不然有不少内容可以交流。
被改的内存是内核里的内存吗?
你们是不是在尝试把内核态的程序移植到RTP?


在 2011年9月24日 下午11:14,Jeff Chen <sheis...@gmail.com>写道:

jinhu wang

unread,
Sep 25, 2011, 10:25:24 PM9/25/11
to pon...@googlegroups.com
题外话:为什么在做架构的时候,不优先用AMP模式,而采用难度和挑战性颇高的SMP模式?


在 2011年9月24日 下午11:14,Jeff Chen <sheis...@gmail.com>写道:

jinhu wang

unread,
Sep 25, 2011, 10:42:10 PM9/25/11
to pon...@googlegroups.com
有些C程序员会在函数体内用goto,函数调用栈内用longjmp。而这些语法的大量使用带来的往往是更高的维护成本。
而异常又是一个这样的一个语法元素。
我相信异常能做很多事情,但是我们换一个角度考虑,是不是可以用更安全更明了的方式来替代异常或者预防异常呢?
例如构造函数失败,失败场景诸如资源分配、参数非法,如果把这些事情在构造之前做好,是不是会更有效。

在 2011年9月24日 下午6:45,Yongwei Wu <wuyo...@gmail.com>写道:

Jeff Chen

unread,
Sep 26, 2011, 11:19:48 AM9/26/11
to pon...@googlegroups.com
把其他DSP中的内容移植到RTO里

Yongwei Wu

unread,
Sep 26, 2011, 11:24:00 AM9/26/11
to pon...@googlegroups.com
合适地使用goto代码仍然是可维护的(常见情况:跳出循环、跳转到结尾处)。
使用longjmp代码是不容易维护的,特别在C++下,longjmp不能对C++对象进行合
适的处理。这在C++里是不可能处理好的。

异常是语言层面的东西。从图灵机的角度来说,没什么是异常做得到,C做不到
的。从表达能力的角度,使用异常能表达一些以前程序员不愿意表达的东西。

就说我前几天举的string的例子。如果任何一次操作都要检查是否有错误,这样
的代码几乎是没法看的。所以传统上C程序员不会这样写代码,会使用固定的缓
冲区大小。C++程序员使用string的话,就会更愿意使用变长的缓冲区。如陈硕
所说,语言可以决定思维。

哪种好,我就不做辩论了。每个人都有自己的风格。C的风格可能可以更高效、
也更直截了当,但更容易导致不检查错误码和缓冲区溢出。“现代”C++的风格
可以更简洁(未必更容易写),异常不检查会导致程序退出,缓冲区溢出也较难
发生。

2011/9/26 jinhu wang <wangji...@gmail.com>:

jinhu wang

unread,
Sep 26, 2011, 11:42:48 PM9/26/11
to pon...@googlegroups.com
哦,代码的降级使用:)
你们之前的DSP应该是用的OSEck吧?应该还是跟内核态的vxworks更接近。另外要注意的是DSP上的内存使用方式也特殊。

Atry

unread,
Sep 27, 2011, 7:33:30 AM9/27/11
to pon...@googlegroups.com
因为有人错用就禁止使用某语法,虽然实践中勉强也可行,但这算是认怂了。

做好管理,认真Review和编码规范,容易用错的语法必须在规范中界定清楚什么时候能用什么时候不能用。

Jeff Chen

unread,
Sep 27, 2011, 9:09:02 AM9/27/11
to pon...@googlegroups.com
Hi
   昨天无法回答你的问题,因为我去年跳槽到现在公司后,就加入这个项目,已经决定用SMP。
   今天查了一下相关文档,了解了原因.
   
1. 因为AMP的核与核之间是完全独立的, 但是外部的resource并不太好分割,这样的话,不能简单的换板子就行(新的支持8core的板子替换原来的板子,原来板子跑的是PSOS),需要改硬件的架构.
2. 在AMP下,每个核在外部看来都是独立的系统.那也就是说,每个核都需要分别加载,初始化,配置. 这样对O&M系统影响比较大,这样也就不能简单的替换板子
3. 由于AMP把任务固定分在各个核里,这样不方便测量. 当把系统更新到更多核的板子上去时,AMP改动比较大,而在SMP上却几乎无需改动
4. SMP更普遍,并且更容易测试. 而且将来也有项目需要SMP,这样可以积累经验.

Jeff Chen

unread,
Sep 27, 2011, 9:11:14 AM9/27/11
to pon...@googlegroups.com
我们相当于把外部DSP上的功能移到新的多核的板子上来

当时有2种方式
1种就是现在采纳的变成RTP
第二种是变成kernel mode的一个或几个task

权衡利弊后,决定还是变成RTP

Jeff Chen

unread,
Sep 27, 2011, 9:13:54 AM9/27/11
to pon...@googlegroups.com
呵呵,你看windriver的内核库里都是goto

jinhu wang

unread,
Sep 28, 2011, 12:11:39 AM9/28/11
to pon...@googlegroups.com
在 2011年9月27日 下午9:09,Jeff Chen <sheis...@gmail.com>写道:
Hi
   昨天无法回答你的问题,因为我去年跳槽到现在公司后,就加入这个项目,已经决定用SMP。
   今天查了一下相关文档,了解了原因.
   
1. 因为AMP的核与核之间是完全独立的, 但是外部的resource并不太好分割,这样的话,不能简单的换板子就行(新的支持8core的板子替换原来的板子,原来板子跑的是PSOS),需要改硬件的架构.
//这个其实不是问题,首先AMP下资源还是能划分的,还是可以设置共享内存做数据交互的。软件模块隔离的效果要比SMP好,至少一个crash不影响别的核。另外SM把多核调度的压力全交给了OS,OS承担了更大的责任和风险。AMP环境下,OS对核的调度还是延续传统久经考验,稳定性肯定好的多。
2. 在AMP下,每个核在外部看来都是独立的系统.那也就是说,每个核都需要分别加载,初始化,配置. 这样对O&M系统影响比较大,这样也就不能简单的替换板子
 //对,要设计一个好的加载流程,这是O&M需要干的。
3. 由于AMP把任务固定分在各个核里,这样不方便测量. 当把系统更新到更多核的板子上去时,AMP改动比较大,而在SMP上却几乎无需改动
 //这个都得具体分析,即便是SMP模式下,调整核的数量也要考虑任务的细粒度划分
 
4. SMP更普遍,并且更容易测试. 而且将来也有项目需要SMP,这样可以积累经验.
 //SMP在嵌入式上成功案例估计不多,以往的做成功的案例大多数还是AMP的模式,因为能更好的跟机架式结构对等。

Jeff Chen

unread,
Sep 28, 2011, 11:01:56 AM9/28/11
to pon...@googlegroups.com
主要还是减少对硬件的影响.

其实我们现在SMP做的比较奇怪.
主要的8个功能,准备affinity到8个核上....

我是反对这样做的,不过system architecture想这么做....

Jeff Chen

unread,
Sep 28, 2011, 11:09:11 AM9/28/11
to pon...@googlegroups.com
这里说的资源分割,是指外部的资源(不属于SMP或者AMP)。

在 2011年9月28日 下午12:11,jinhu wang <wangji...@gmail.com>写道:

jinhu wang

unread,
Sep 28, 2011, 3:27:11 PM9/28/11
to pon...@googlegroups.com
看来问题的症结还是出在系统架构师,政治因素大于技术因素。

Zhangming Niu

unread,
Sep 28, 2011, 3:43:17 PM9/28/11
to pon...@googlegroups.com
哪里有政治了。。

2011/9/28 jinhu wang <wangji...@gmail.com>
Reply all
Reply to author
Forward
0 new messages