首先,我坚定的认为应该用异常,应该使用异常作为错误报告的唯一机制。哦,实际上,不是我,是Brad Abrams(《.NET框架设计指南》作者),Anders(C#之父),Andrei Alexandrescu(《C++ Coding Standard: 101 Rules》作者)这么说的。
其次,国内的C++开发者估计都知道,在绝大多数C++项目中,异常都是被禁用的,至于是不是连编译选项都关掉了还是只是在编码规定上禁止,就不知道了(你那边的实际情况是什么?:-)),据说就连Google内部也是禁用异常的。所以估计情况还不仅仅局限于国内,甚至地球上的C++项目大抵都是如此。
然而不可否认的一个事实是,C++社群这么多年来的经验告诉我们,使用异常(具体参见《C++ Coding Standard: 101 Rules》第7x章~第7y章(x=?和y=?我忘了))。Herb Sutter和Andrei Alexandrescu在C++异常方面的经验和造诣估计C++社群还没有几个人能够质疑的。
生下来的问题就很简单了:为什么存在这个理想与现实的矛盾?这就是我想请大家讨论的:P
个人一点看法:
1. Fallacies和Myths导致的Fear心态,进而导致在异常的正确实践方式方面的标准缺乏(《C++ Coding Standard: 101 Rules》出版之前,你能找到一份哪怕比较标准的,权威的,关于如何,何时使用异常的参考文献吗?)
2. 旧时代异常实现手段的落后(g++的影栈(shadow stack)实现)以及硬件的落后(导致异常开销*相对*显得大),致使在当时,异常被打入冷宫。由于人在心理上容易忘记一个论点的前提而记住其结论,所以若干年后的今天,仍然有大量人认为异常的开销在任何情况下都很大很显著。而实际情况是,异常有开销,但仅当在极度追求效率(时间&空间)的场合(极少),这个开销是完全可以忽略的。
3. 教育问题。异常在C++里面是一个较新的特性。而且,使用错误代码,不需要新学习语言特性,只要在现有语言特性(返回值)上hack就行了。不得不承认,就连程序员也是害怕学新东西的。
4. 把错误处理的本质困难错误地归咎到异常身上。错误处理的本质困难是:定义哪些情况属于错误,错误发生的时候在什么地点响应,错误发生如何确保操作的完整语意(回滚、保存数据、释放资源)。无论用异常还是错误代码,这些问题都同样尖锐。
5. 更多重量级的Fallacies:详见Raymond Chen(《The Old New Thing》作者,Win32大牛)和Joel Spolsky(《Joel On Software》作者)的口水。Raymond Chen的" Cleaner, more elegant, and harder to recognize ",和Joel的两篇,这里和 这里。要知道,这两位的口水的权重可不是一般的大啊。幸亏人民群众的眼睛是雪亮的,Raymond和Joel的论点乍看十分、非常有理,实际经不起推敲。你能看出里面的漏洞吗?
6. 遗留代码问题。遗留代码里面没有用异常,导致往遗留系统里面增加新模块也首先与原先系统里用的错误处理策略的限制(这一点我不是很确定,有实际经验的可以明确的举例说明一下?)
7. 心理问题。大量C++程序员有C背景(也许是因为教材是从C往上写的也许其他什么原因),从心里就没有把异常当成一个first-class的,专门的,唯一的错误处理机制。Java通过把人们逼上梁山解决了这个心理问题。而对于C++程序员来说,也许下意识里面最"熟悉"的就是用返回值来报错;至于找出一堆理由...well...只是找出一堆理由而已。能找出一堆经得起推敲的理由,那才是真的理由。不过,对自己找的理由从来都不需要推敲的。
最想知道的就是大家经历的实际项目中的情况,越明确,越实际越好:-) 我似乎把空话套话都给说完了:P
--
刘未鹏(pongba)|C++的罗浮宫
http://blog.csdn.net/pongba
TopLanguage
http://groups.google.com/group/pongba
但是用异常的话, 会不会造成资源泄漏或者其他不稳定的因素, 可就不是堆砌人力
或者分析软件可以分析出来的了.
我的想法, 不用异常的话, 程序质量的保证更容易由非天才工程师保证; 用了异常
的话, 嗯, 我们只能期待每个角落的编码, 都考虑到异常的 10 rules 了.
pongba 写道:
不用异常的话, 某个函数的返回值是否被检查, 是容易用人工检查的(外包给别人
检查都可以), 也容易用程序质量分析软件处理(国外有这样的软件/service), 其
实可以保证到每个函数的返回值都被检查, 甚至可以保证到函数的每个错误值都被
检查.
但是用异常的话, 会不会造成资源泄漏或者其他不稳定的因素, 可就不是堆砌人力
或者分析软件可以分析出来的了.
另一个,整个系统是基于返回值的,导致后面在用的时候也主要去想怎么用返回值描述了。
不过我做的系统倒是有一个地方很有意思,如果new失败,也不返回null,也不抛异常,而是系统直接吐核了,相当于是个操作系统级别的异常吧......
一个是最早学的时候就没有很好的异常教材,语法书里只是讲了怎么用异常,而没有讲什么是异常。真正讲什么是异常的书到现在也就那么几本吧。而且真正要想
正确区分错误和异常,在实践上感觉还是比较难的,有可能出现前期发现是异常,后面却要转成错误,或者相反的情况。毕竟对一个新项目来说,前期分析不可能
做到尽善尽美。
On 10/26/07, red...@gmail.com <red...@gmail.com> wrote:
不 用异常的话, 某个函数的返回值是否被检查, 是容易用人工检查的(外包给别人
检查都可以), 也容易用程序质量分析软件处理(国外有这样的软件/service), 其
实可以保证到每个函数的返回值都被检查, 甚至可以保证到函数的每个错误值都被
检查.
但是用异常的话, 会不会造成资源泄漏或者其他不稳定的因素, 可就不是堆砌人力
或者分析软件可以分析出来的了.
不用异常的话, 某个函数的返回值是否被检查, 是容易用人工检查的(外包给别人
检查都可以), 也容易用程序质量分析软件处理(国外有这样的软件/service), 其
实可以保证到每个函数的返回值都被检查, 甚至可以保证到函数的每个错误值都被
检查.
但是用异常的话, 会不会造成资源泄漏或者其他不稳定的因素, 可就不是堆砌人力
或者分析软件可以分析出来的了.
我的想法, 不用异常的话, 程序质量的保证更容易由非天才工程师保证; 用了异常
的话, 嗯, 我们只能期待每个角落的编码, 都考虑到异常的 10 rules 了.
不是 catch 的问题, 而是 throw 和 catch 中间路径的代码, 是不是都是异常安全的, 这个检查起来比较费事.
BOOL ComputeChecksum(LPCTSTR pszFile, DWORD* pdwResult)这段代码做到错误安全了吗?不管是人眼,还是工具,看到这段代码唯一能够得出的结论是:写代码的人*想要*做到错误安全。然而"写代码的人想不想做到错误安全"并不是真正重要的问题,重要的是,代码实际上是不是异常安全的。
{
BOOL fRc = FALSE;
HANDLE h = CreateFile(pszFile, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h != INVALID_HANDLE_VALUE) {
HANDLE hfm = CreateFileMapping(h, NULL, PAGE_READ, 0, 0, NULL);
if (hfm) {
void *pv = MapViewOfFile(hfm, FILE_MAP_READ, 0, 0, 0);
if (pv) {
DWORD dwHeaderSum;
if (CheckSumMappedFile(pvBase, GetFileSize(h, NULL),
&dwHeaderSum, pdwResult)) {
fRc = TRUE;
}
UnmapViewOfFile(pv);
}
CloseHandle(hfm);
}
CloseHandle(h);
}
return fRc;
}
返回值的检查与否, 是静态路径, 局部化的; 但是异常安全, 是动态的, 全局的, 检查代码是否ok 麻烦很多.
例如, 析构函数是不应该让异常跑出去的, 如果一个模板类的析构函数要调用 policy 中的一个函数, 此时要不要 try, catch ? 如果每个析构函数都要写上 try, catch, 是不是代码其实也很烦 ? 但是你有办法吗? 你无法预料别人给你一个什么样的 policy.
这里, 反而返回值可能还简单一些, 反正之后相关的东西都不用了, 忽略掉似乎关系也不大.
异常还涉及到 API 的设计, 不是有些容器, 某些功能合起来一起好用, 但是提供的 api 是分开的, 为了异常安全吗 ? 写个什么东西都要考虑这么多, 这不是 "心智负担" 吗? 这些与语义有关的东西, 人工检查不容易, 开发自动软件好像也不太容易吧 ?
你可能说, 要求每个程序员都做好 10 rules 就行, 但是, 如果不是整个团队水平和责任心都这么好呢? 如果碰到赶工想快点交货呢 ? 如果要使用一个第三方库呢 ? 如果要调用一个 C 库, 并且还要给 C 库回调函数呢 ?
我觉得, 异常带来了优雅, 但是同时带来了很大的负担.
不用异常, 代码的表面罗嗦, 但是却容易保证正确性.
不是 catch 的问题, 而是 throw 和 catch 中间路径的代码, 是不是都是异常安全的, 这个检查起来比较费事.
返回值的检查与否, 是静态路径, 局部化的; 但是异常安全, 是动态的, 全局的, 检查代码是否ok 麻烦很多.
例如, 析构函数是不应该让异常跑出去的, 如果一个模板类的析构函数要调用 policy 中的一个函数, 此时要不要 try, catch ? 如果每个析构函数都要写上 try, catch, 是不是代码其实也很烦 ? 但是你有办法吗? 你无法预料别人给你一个什么样的 policy.
这里, 反而返回值可能还简单一些, 反正之后相关的东西都不用了, 忽略掉似乎关系也不大.
异常还涉及到 API 的设计, 不是有些容器, 某些功能合起来一起好用, 但是提供的 api 是分开的, 为了异常安全吗 ? 写个什么东西都要考虑这么多, 这不是 "心智负担" 吗? 这些与语义有关的东西, 人工检查不容易, 开发自动软件好像也不太容易吧 ?
你可能说, 要求每个程序员都做好 10 rules 就行, 但是, 如果不是整个团队水平和责任心都这么好呢? 如果碰到赶工想快点交货呢 ? 如果要使用一个第三方库呢 ? 如果要调用一个 C 库, 并且还要给 C 库回调函数呢 ?
我觉得, 异常带来了优雅, 但是同时带来了很大的负担.
不用异常, 代码的表面罗嗦, 但是却容易保证正确性.
pongba 写道:
On 10/26/07, red...@gmail.com < red...@gmail.com> wrote:呃.. 从理论上,是可以做出这样的工具:它能够检查一个程序中所有潜在抛出的异常是否都被catch了。目前有没有这个工具我不知道,但根据异常在其它语言中的 繁荣程度,至少不应该不会出现。只要检查是否所有异常都被catch,是否有空catch,是否有无副作用的catch,等等就可以一定程度上保证软件没 有无视异常了吧。因此我觉得这个论据不成立。不 用异常的话, 某个函数的返回值是否被检查, 是容易用人工检查的(外包给别人
检查都可以), 也容易用程序质量分析软件处理(国外有这样的软件/service), 其
实可以保证到每个函数的返回值都被检查, 甚至可以保证到函数的每个错误值都被
检查.
但是用异常的话, 会不会造成资源泄漏或者其他不稳定的因素, 可就不是堆砌人力
或者分析软件可以分析出来的了.
异常并不是你不用就不存在的,如果你有一个第三方的库它使用异常,你怎么弄?以返回值返回错误的函数,你们确保都能被程序员检测吗?一个不太恰当的问题,有多少人检测过MessageBox的返回值?所以返回值返回我看悬。
> 至于异常的捕捉,如果一个异常未被捕捉,那么最终会造成一个程序中止或系统中止。这是显式的,完整的测试可以消除绝大部分的这类问题,即使有遗漏,系统也不会悄 悄地"带病工作"。相反,如果一个返回错误码未被捕捉,则不会有任何反应。问题是隐式的,严格的测试也无法保证能够发现这类问题。更不用说运行时,程序的奇怪行 为了。
>
> 在07-10-26,red...@gmail.com <red...@gmail.com> 写道:
>
>
>
>
>
>
>
> > 不用异常的话, 某个函数的返回值是否被检查, 是容易用人工检查的(外包给别人
> > 检查都可以), 也容易用程序质量分析软件处理(国外有这样的软件/service), 其
> > 实可以保证到每个函数的返回值都被检查, 甚至可以保证到函数的每个错误值都被
> > 检查.
>
> > 但是用异常的话, 会不会造成资源泄漏或者其他不稳定的因素, 可就不是堆砌人力
> > 或者分析软件可以分析出来的了.
>
> > 我的想法, 不用异常的话, 程序质量的保证更容易由非天才工程师保证; 用了异常
> > 的话, 嗯, 我们只能期待每个角落的编码, 都考虑到异常的 10 rules 了.
>
> > pongba 写道:
> > > 首先,我坚定的认为应该用异常,应该使用异常作为错误报告的唯一机制。哦,
> > > 实际上,不是我,是Brad Abrams(《.NET框架设计指南》作者),Anders(C#之
> > > 父),Andrei Alexandrescu(《C++ Coding Standard: 101 Rules》作者)这么
> > > 说的。
>
> --
> 反者道之动,弱者道之用
> m...@seaskysh.com
> longshank...@gmail.comhttp://blog.csdn.net/longshanks/- Hide quoted text -
>
> - Show quoted text -
这样起码能够避免大多数的错误, 如果写不出这样的代码, 则证明有些 api 设计
不合理, 要考虑重新设计.
这种规定, 是容易检查是否符合的, 而且不必高手就可以检查, 并且工具检查也不
是很难.
-----
如果我规定
大家一定要写异常安全代码,
我的细则应该怎么写?
一个函数内部, 应该先写可能抛出异常的代码, 然后修改状态
缺省构造函数应该如何如何
析构函数应该如何如何
拷贝构造函数应该如何如何
赋值构造函数应该如何如何
重载了 new 应该如何如何
重载了 delete 应该如何如何
返回临时对象的函数应该如何如何 ?
如果是模板中, 返回模板参数中的类型, 又应该怎么办 ?
写类似stl 中 pop_back 之类的函数应该如何如何 ?
......
那么细则太多条了, 不但设计到函数内部的代码, 还设计到 api 设计.
检查代码的时候, 很辛苦啊.
在07-10-26,red...@gmail.com <red...@gmail.com> 写道:不是 catch 的问题, 而是 throw 和 catch 中间路径的代码, 是不是都是异常安全的, 这个检查起来比较费事.
这方面,异常的确复杂于返回码。但是,解决的方法却是相同的,即资源的自动化管理。这是一劳永逸的方法,对于两种形式都有作用。
莫华枫 写道:不仅仅是资源管理完毕了就好了啊, 例如, 我往你的 list 类里面插一个对象, 你的list 做了一些内部事情, 然后执行 = 赋值这个对象, 此时异常发生, 仅仅资源不泄漏, 但是内部状态改掉了.
在07-10-26,red...@gmail.com <red...@gmail.com> 写道:不是 catch 的问题, 而是 throw 和 catch 中间路径的代码, 是不是都是异常安全的, 这个检查起来比较费事.
这方面,异常的确复杂于返回码。但是,解决的方法却是相同的,即资源的自动化管理。这是一劳永逸的方法,对于两种形式都有作用。
关键是异常是只要你调用了外部的函数就可能发生, 甚至一个操作符也可能发生, 时时刻刻都要注意, 同时规则又多, 我比较害怕.
(不重载操作符的日子, 要轻松一些, 容易知道, 这几行代码是不会跑个异常出来吓人一跳. )
关键是异常是只要你调用了外部的函数就可能发生, 甚至一个操作符也可能发生, 时时刻刻都要注意, 同时规则又多, 我比较害怕.
这个问题同样套在非异常方式上,赋值发生错误,如何维持commit or rollback语义呢?这里所需的规则同使用异常时的有多大的差别呢?
(不重载操作符的日子, 要轻松一些, 容易知道, 这几行代码是不会跑个异常出来吓人一跳. )
因此, 有了 gc, 如果要求不是特别高, 日子还是好了很多, 起码多数要释放的是
内存资源, 问题不大, 一部分非内存资源也能随gc 释放, 但是不能保证全部都会
gc 释放. 要写很可靠的程序, 这个不能保证, 和没有这个功能, 似乎差得不是很远啊.
莫华枫 写道:不仅仅是资源管理完毕了就好了啊, 例如, 我往你的 list 类里面插一个对象, 你的list 做了一些内部事情, 然后执行 = 赋值这个对象, 此时异常发生, 仅仅资源不泄漏, 但是内部状态改掉了.
在07-10-26,red...@gmail.com <red...@gmail.com> 写道:不是 catch 的问题, 而是 throw 和 catch 中间路径的代码, 是不是都是异常安全的, 这个检查起来比较费事.
这方面,异常的确复杂于返回码。但是,解决的方法却是相同的,即资源的自动化管理。这是一劳永逸的方法,对于两种形式都有作用。
关键是异常是只要你调用了外部的函数就可能发生, 甚至一个操作符也可能发生, 时时刻刻都要注意, 同时规则又多, 我比较害怕.
(不重载操作符的日子, 要轻松一些, 容易知道, 这几行代码是不会跑个异常出来吓人一跳. )
如果返回值结合 RAII, 其实也不复杂啊, 用 RAII 管理数据, 发现返回值错误,
直接就返回自己的错误值, 不就可以了, 更简单.
>
> 1. 使用RAII来包装每一个资源(很多时候可以利用智能指针类来免于自己手写
> 包装类)。
> 1'. 如果用D的话,在每一个资源申请下面加上相应的scope(exit)子句。
>
> 2. 析构函数不许抛出异常。
>
> 3. 运用copy-swap手法实现异常安全的copy-assignment操作符。
>
> OK,我觉得最常见场景需要知道的essential rules就以上这些了。完全的Rules
> 列表也并没有多少,《C++ Coding Standards》上面写得一清二楚。
还有一条你没有实现啊, 就是错误发生的时候, 不要影响对象的状态.
用返回值规则的话, 只要调用完所有需要检查返回值的函数, 都没有返回值都通过
检查了, 接着就可以调用不需要检查返回值的函数, 以及直接修改持久性的变量了.
如果用异常的话, 你要一个一个确认某个函数会不会产生异常, 检查和对象有关的
每个操作符, 是否被重载掉了, 重载函数会不会抛异常, 才能决定应该如何写这段
代码, 检查工作多了很多啊.
(所以我不重载操作符, 我胆小).
如果返回值结合 RAII, 其实也不复杂啊, 用 RAII 管理数据, 发现返回值错误,
直接就返回自己的错误值, 不就可以了, 更简单.
>
> 1. 使用RAII来包装每一个资源(很多时候可以利用智能指针类来免于自己手写
> 包装类)。
> 1'. 如果用D的话,在每一个资源申请下面加上相应的scope(exit)子句。
>
> 2. 析构函数不许抛出异常。
>
> 3. 运用copy-swap手法实现异常安全的copy-assignment操作符。
>
> OK,我觉得最常见场景需要知道的essential rules就以上这些了。完全的Rules
> 列表也并没有多少,《C++ Coding Standards》上面写得一清二楚。
还有一条你没有实现啊, 就是错误发生的时候, 不要影响对象的状态.
用返回值规则的话, 只要调用完所有需要检查返回值的函数, 函数没有return
with err, 接着就可以调用不需要检查返回值的函数状态修改函数, 以及修改持久
> 鉴于你对使用返回值构造的条例主要针对的是资源管理,我也试试对使用异常的
> 情况下如何正确管理资源制定条例:
这是很多年前写的代码了, 那时还没有怎么用 RAII.
去年用 C++ 代码的时候, 就比较懒, 不管函数不好看, 直接在函数内部定义用于
RAII 的局部类.
如果返回值结合 RAII, 其实也不复杂啊, 用 RAII 管理数据, 发现返回值错误,
直接就返回自己的错误值, 不就可以了, 更简单.
>
> 1. 使用RAII来包装每一个资源(很多时候可以利用智能指针类来免于自己手写
> 包装类)。
> 1'. 如果用D的话,在每一个资源申请下面加上相应的scope(exit)子句。
>
> 2. 析构函数不许抛出异常。
>
> 3. 运用copy-swap手法实现异常安全的copy-assignment操作符。
>
> OK,我觉得最常见场景需要知道的essential rules就以上这些了。完全的Rules
> 列表也并没有多少,《C++ Coding Standards》上面写得一清二楚。
还有一条你没有实现啊, 就是错误发生的时候, 不要影响对象的状态.
用返回值规则的话, 只要调用完所有需要检查返回值的函数, 函数没有return
with err, 接着就可以调用不需要检查返回值的函数状态修改函数, 以及修改持久
性的变量了.
如果用异常的话, 你要一个一个确认某个函数会不会产生异常, 检查和对象有关的
每个操作符, 是否被重载掉了, 重载函数会不会抛异常, 才能决定应该如何写这段
代码, 检查工作多了很多啊.
(所以我不重载操作符, 我胆小).
整个软件架设在异常的出错机制基础上确实有时候比较爽,但由此带来的复杂性也不算少。
使用异常,你必须能自己控制异常,而且对异常和各种状态都有所理解,但使用错误,不需要考虑太多的,最多是些机械的劳动而已。
我们调用的库本身也各种风格,小组内成员也有可能水平参差不齐,强制使用错误返回能省不少的事情。
当然要是自信自己所在的团队能驾预异常,那也未尝不可。我认为异常并不适合所有的情况,我们得权衡和根据实际情况作出选择。
首先,我坚定的认为应该用异常,应该使用异常作为错误报告的唯一机制。哦,实际上,不是我,是Brad Abrams(《.NET框架设计指南》作者),Anders(C#之父),Andrei Alexandrescu(《C++ Coding Standard: 101 Rules》作者)这么说的。
其次,国内的C++开发者估计都知道,在绝大多数C++项目中,异常都是被禁用的,至于是不是连编译选项都关掉了还是只是在编码规定上禁止,就不知道了(你那边的实际情况是什么?:-)),据说就连Google内部也是禁用异常的。所以估计情况还不仅仅局限于国内,甚至地球上的C++项目大抵都是如此。
然而不可否认的一个事实是,C++社群这么多年来的经验告诉我们,使用异常(具体参见《C++ Coding Standard: 101 Rules》第7x章~第7y章(x=?和y=?我忘了))。Herb Sutter和Andrei Alexandrescu在C++异常方面的经验和造诣估计C++社群还没有几个人能够质疑的。
生下来的问题就很简单了:为什么存在这个理想与现实的矛盾?这就是我想请大家讨论的:P
个人一点看法:
1. Fallacies和Myths导致的Fear心态,进而导致在异常的正确实践方式方面的标准缺乏(《C++ Coding Standard: 101 Rules》出版之前,你能找到一份哪怕比较标准的,权威的,关于如何,何时使用异常的参考文献吗?)
2. 旧时代异常实现手段的落后(g++的影栈(shadow stack)实现)以及硬件的落后(导致异常开销*相对*显得大),致使在当时,异常被打入冷宫。由于人在心理上容易忘记一个论点的前提而记住其结论,所以若干年后的今天,仍然有大量人认为异常的开销在任何情况下都很大很显著。而实际情况是,异常有开销,但仅当在极度追求效率(时间&空间)的场合(极少),这个开销是完全可以忽略的。
3. 教育问题。异常在C++里面是一个较新的特性。而且,使用错误代码,不需要新学习语言特性,只要在现有语言特性(返回值)上hack就行了。不得不承认,就连程序员也是害怕学新东西的。
4. 把错误处理的本质困难错误地归咎到异常身上。错误处理的本质困难是:定义哪些情况属于错误,错误发生的时候在什么地点响应,错误发生如何确保操作的完整语意(回滚、保存数据、释放资源)。无论用异常还是错误代码,这些问题都同样尖锐。
5. 更多重量级的Fallacies:详见Raymond Chen(《The Old New Thing》作者,Win32大牛)和Joel Spolsky(《Joel On Software》作者)的口水。Raymond Chen的" Cleaner, more elegant, and harder to recognize ",和Joel的两篇,这里和 这里。要知道,这两位的口水的权重可不是一般的大啊。幸亏人民群众的眼睛是雪亮的,Raymond和Joel的论点乍看十分、非常有理,实际经不起推敲。你能看出里面的漏洞吗?
6. 遗留代码问题。遗留代码里面没有用异常,导致往遗留系统里面增加新模块也首先与原先系统里用的错误处理策略的限制(这一点我不是很确定,有实际经验的可以明确的举例说明一下?)
7. 心理问题。大量C++程序员有C背景(也许是因为教材是从C往上写的也许其他什么原因),从心里就没有把异常当成一个first-class的,专门的,唯一的错误处理机制。Java通过把人们逼上梁山解决了这个心理问题。而对于C++程序员来说,也许下意识里面最"熟悉"的就是用返回值来报错;至于找出一堆理由...well...只是找出一堆理由而已。能找出一堆经得起推敲的理由,那才是真的理由。不过,对自己找的理由从来都不需要推敲的。
最想知道的就是大家经历的实际项目中的情况,越明确,越实际越好:-) 我似乎把空话套话都给说完了:P
--
刘未鹏(pongba)|C++的罗浮宫
最主要还是判断什么时候该用,什么时候不该用。
整个软件架设在异常的出错机制基础上确实有时候比较爽,但由此带来的复杂性也不算少。
使用异常,你必须能自己控制异常,而且对异常和各种状态都有所理解,但使用错误,不需要考虑太多的,最多是些机械的劳动而已。
我们调用的库本身也各种风格,小组内成员也有可能水平参差不齐,强制使用错误返回能省不少的事情。
当然要是自信自己所在的团队能驾预异常,那也未尝不可。我认为异常并不适合所有的情况,我们得权衡和根据实际情况作出选择。
人肉GC比之人肉compiler,难度还是要稍小一些
在团队中使用异常,就需要大部分人都具有人肉compiler的实力;而使用error code,则只需要小部分的人肉GC……
不是 catch 的问题, 而是 throw 和 catch 中间路径的代码, 是不是都是异常安全的, 这个检查起来比较费事.
BOOL ComputeChecksum(LPCTSTR pszFile, DWORD* pdwResult)
{
BOOL fRc = FALSE;
HANDLE h = CreateFile(pszFile, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h != INVALID_HANDLE_VALUE) {
HANDLE hfm = CreateFileMapping(h, NULL, PAGE_READ, 0, 0, NULL);
if (hfm) {
void *pv = MapViewOfFile(hfm, FILE_MAP_READ, 0, 0, 0);
if (pv) {
DWORD dwHeaderSum;
if (CheckSumMappedFile(pvBase, GetFileSize(h, NULL),
&dwHeaderSum, pdwResult)) {
fRc = TRUE;
}
UnmapViewOfFile(pv);
}
CloseHandle(hfm);
}
CloseHandle(h);
}
return fRc;
}
这种goto用法我并不觉得有多少不好, 我不求代码 goto-free.
再考虑这个问题:
如果由于程序库或者业务发展的需要,
1. 返回值方式: 某个原来没有返回值的函数, 现在有返回值了
2. 异常方式: 某个原来自己不throw, 调用的函数自己也不throw 的函数, 现在会
throw 了
看看我们应该怎么处理:
1. 错误值方式
全局 search 所有调用该函数的地方, 在每个调用, 它处于 "最后做安全的事情"
的那部分, 那么应该移到前面 "可能失败" 的部分, 并且检查返回值.
有可能某个调用者, 原来不返回值, 现在它自己处理不了这个错误的话, 那么也要
改成返回值. 这个事情的处理是递归的.
如果它被用来赋值给某个函数指针, 那么现在这个赋值应该编译不过, 需要修改函
数指针定义, 再递归处理函数指针的调用.
函数指针的调用者的递归处理
如果某个多态函数, 基类函数中没有返回值, 这不应该抛出异常, 那么只要任何派
生类函数调用了上面说到的函数, 而自己又不能处理掉错误, 那么这套函数就应该
变成有返回值, 将基类函数返回值改掉, 同时该函数如果不是abstract virtual,
那么临时修改成 abstract virtual 编译一下, 能够将所有需要修改的点给你找出来.
所有这些调用者的递归处理.
2. 异常方式
nothrow 现在不是 c++ 标准吧? 异常抛出规范现在也好像用处也不大, 我们全部
不管这些.
全局 search 所有调用该函数的地方, 这个和上面一样.
如果某个调用者, 原来是不抛出异常的, 现在抛出异常了, 需要递归检查, 这个也
和上面一样.
对于函数指针, 要进行同样的检查, 不过这次编译器不能提醒你了.
还有一个编译器提醒不了的地方:
如果某个多态函数, 基类函数中写明, 这不应该抛出异常, 那么只要任何派生类函
数调用了上面说到的函数, 而自己又不能处理掉异常, 那么这套函数就应该变成会
抛出异常的函数, 所有调用者代码需要改掉. 这下没有什么帮助可以迅速找出所有
要修改的点来了,只有全局 search 了. 希望没有哪个地方, 通过模板参数传进这
个基类, 然后参数传递给另外一个模板, 在后者的函数当中, 派生了这个类 (这个
用法听起来很变态, 但是我写一些辅助功能的小代码的时候, 确实用过).
比较起来, 返回值方式是很直白, 代码不漂亮, 不过用自动化工具确实容易处理很多.
------------------------------------
哎, 写代码还是累啊, 光是一个返回值/异常 throw 的修改, 后面搞不好就是一大
堆工作.
本来要写出合格健壮的程序来(无论是哪个语言), 已经不容易了, 要高清楚业务领
域的事情, 要搞清楚底层平台的时候, 如果语言再给加上一些随时要记住的规则,
多一条都很累人的.
莫华枫 写到建筑业的规则大家都是记得住的. 我倒是觉得, 其实我见过很多建筑
都有很多毛病, 整体的外观设计对内部居住的某些方面也有不良影响, 如果换到软
件行业, 肯定是需要 ---- 重构的.
wang xin 写道:
On Oct 27, 3:36 am, pongba <pon...@gmail.com> wrote:
> 我总结一下,目前最主要的论点是cber和redsea的,认为错误代码更容易检查正确性,异常代码不容易检查.
>
> 我不同意 :-) 观点如下:
>
> On 10/27/07, wang xin <cber.wang...@gmail.com> wrote:
>
>
>
> > 人肉GC比之人肉compiler,难度还是要稍小一些
> > 在团队中使用异常,就需要大部分人都具有人肉compiler的实力;而使用error code,则只需要小部分的人肉GC......
>
> On 10/26/07, red...@gmail.com <red...@gmail.com> wrote:
>
>
>
> > 不是 catch 的问题, 而是 throw 和 catch 中间路径的代码, 是不是都是异常安全的, 这个检查起来比较费事.
>
> 用错误代码的话,检查起来同样费事。
>
> 因为"检查了每个返回值"并不代表"做到了错误安全",而这个正是Raymond的那篇post犯的错误,比如:
>
> BOOL ComputeChecksum(LPCTSTR pszFile, DWORD* pdwResult)
> {
> BOOL fRc = FALSE;
> HANDLE h = CreateFile(pszFile, GENERIC_READ, FILE_SHARE_READ,
> NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
>
> if (h != INVALID_HANDLE_VALUE) {
> HANDLE hfm = CreateFileMapping(h, NULL, PAGE_READ, 0, 0, NULL);
> if (hfm) {
> void *pv = MapViewOfFile(hfm, FILE_MAP_READ, 0, 0, 0);
> if (pv) {
> DWORD dwHeaderSum;
> if (CheckSumMappedFile(pvBase, GetFileSize(h, NULL),
> &dwHeaderSum, pdwResult)) {
> fRc = TRUE;
> }
> UnmapViewOfFile(pv);
> }
> CloseHandle(hfm);
> }
> CloseHandle(h);
> }
> return fRc;
>
> }
>
> 这段代码做到错误安全了吗?不管是人眼,还是工具,看到这段代码唯一能够得出的结论是:写代码的人*想要*做到错误安全。然而真正重要的问题却是"代码*实际* 是否是错误安全的",后者能够用工具check出来吗?
在 2007-10-27六的 00:37 -0700,abware写道:
--
Live Long and Prosper
- oldrev
> > 际上每个人都会.就是if /else,控制流而已.所以这里真正的问题是"大家都不会用",而不是"这个工具不好".
> > 此外,关于错误代码or异常的决策并不能仅看一个方面,还要看其他的异常的优点, 做全面的权衡.一旦往全面了考虑,异常的优点是显而易见的.
> >
> > 但我也的确认为,尤其是在C++里面,我怀疑10个有9个根本不知道异常的最佳实践是什么,也许程序员的合格性才是真正的问题,人的问题.其实C++
> > 异常的最佳实践也不复杂,为什么会出现这个问题可能还是要归结为教育问题.而教育问题估计又要归结为业界长期的谬误流传.C++
> > 当初加入异常几乎是主流语言第一个吃螃蟹的, 退缩心理自然不会少.口口相传,就形成了不健康的亚文化.我得承认
> > ,我自己接触C++这么长时间,也从来没有觉得错误处理是个重要的问题.(估计就算是其他主流语言的程序员也是如此).
> > 话说回来,10个有9个半不会用异常(这里不存在"有点会用"这个灰色区域,错误处理要么成功要么失败,有点会用也是白搭),但如果用错误代码呢?实
> > 际上每个人都会.就是if /else,控制流而已.所以这里真正的问题是"大家都不会用",而不是"这个工具不好".
> >
> > --
> > 刘未鹏(pongba)|C++的罗浮宫http://blog.csdn.net/pongba
> > TopLanguagehttp://groups.google.com/group/pongba
>
> >
--
Live Long and Prosper
- oldrev
--
Everything is possible and available if we trust ourselves!
话也可以反过来说:有了充分的测试,使用异常又如何?关键是有没有充分的测试?
在07-10-27,wang xin <cber.w...@gmail.com > 写道:对,有了充分的测试,即便不用异常又如何
"异常",顾名思义,就是不正常的情况,unhappy path在一个程序的运行中出现的概率实在是太低了(当然,如果频率高的话,就可以用一条happy path来取代它);再说了,又不是所有的程序都要求24 X 7,像我们开发的那个server程序,即便用户高达4xxx家(不乏一些要求很苛刻的用户,如:美国海军、DoD等……),依然有若干fatal error运行程序崩溃。
在07-10-27,oldrev < old...@gmail.com > 写道:认为用返回值比异常能够更好地保证错误安全其实和认为用静态语言比动态语言开
发的程序质量高一样是不可信的,唯一可信的是测试。
测试是在编码之后进行的,使用异常带来的结果就是编码过程中的难度变大,而用error code就没有这个问题了,即便是水平一般的程序员,也可以很简单的把程序给调试通过并提交给测试用例
对,有了充分的测试,即便不用异常又如何
"异常",顾名思义,就是不正常的情况,unhappy path在一个程序的运行中出现的概率实在是太低了(当然,如果频率高的话,就可以用一条happy path来取代它);再说了,又不是所有的程序都要求24 X 7,像我们开发的那个server程序,即便用户高达4xxx家(不乏一些要求很苛刻的用户,如:美国海军、DoD等……),依然有若干fatal error运行程序崩溃。
On 10/27/07, wang xin < cber.w...@gmail.com> wrote:对,有了充分的测试,即便不用异常又如何
"异常",顾名思义,就是不正常的情况,unhappy path在一个程序的运行中出现的概率实在是太低了(当然,如果频率高的话,就可以用一条happy path来取代它);再说了,又不是所有的程序都要求24 X 7,像我们开发的那个server程序,即便用户高达4xxx家(不乏一些要求很苛刻的用户,如:美国海军、DoD等……),依然有若干fatal error运行程序崩溃。
对了,我对这个现象很有兴趣。
你说"依然有若干fatal error运行程序崩溃",我想知道的是,这个崩溃是像"非法访问"那样的"被动"崩溃,还是check到一个不可恢复的错误状态然后abort()那样的"主动"崩溃?
就让我假定是前者吧(如果猜错了,纠正我:-))。如果是前者,那么几乎肯定是程序早在崩溃点之前就已经发生了错误状态感染了,为什么在当即点上没有check出错误并abort()?跟错误处理机制有关吗?我们知道的是,如果一个应用自底向上使用异常来报告错误,那么任何错误都能够在第一时间用异常抛出去,之后一直到遇到catch子句,是不会被动崩溃的(假定stack-unwinding不会抛出新异常——这是必须遵循的编码原则)。那么如果我们再一个功能模块的顶层用一个general catch来abort模块,便可以做到在该模块中的任何错误都是主动崩溃,而不是被动崩溃。
这个逻辑对不对?也就是说,我的意思是,如果用异常的话,理想情况下是不会出现nasty的"你的程序执行了非法操作,windows将关闭它"这样的错误的,取而代之的是体面的提示对话框,因为在general catch子句中,程序仍然在可用状态。
--
刘未鹏(pongba)|C++的罗浮宫
http://blog.csdn.net/pongba
TopLanguage
错误发生的来源只有两种:一,外界输入。二,内部编程逻辑错误(如边界条件等)。
OOSC(《面向对象软件构造》,Meyer)中提到,用契约式编程,并在违反任何契约的地方抛出异常,从本质上就可以将一切错误在其源头用异常抛出。
加上stack-unwinding过程中不抛出新异常。
这不就做到了?
其中的困难在于,要enforce契约式编程,需要严格的编码素质。但这跟错误代码的对比好处就在于,不用手动传播错误,也不会产生错误被忽视。注意,这 里的重点是,无需任何人力,就省去了"确保错误没被忽视(要review来确保)并被传播"这一环的所有工夫。
为什么程序会被动崩溃,我的推理是就是因为没有在错误的发生点上立即抛出异常从而使执行流不可能带着错误感染状态往下执行。《Why Programs Fail》里面也提到,大量错误在发生的时候早就离发生点十万八千里了,结果就要一系列的推导。我的认为是,如果按照OOSC的方法,一旦违反契约便以异 常方式抛出,就不存在这个问题了。
但是, 可能有一个你内部已经 catch 的异常(从而你也没有对它多害怕), 它的路
径中引发了内存泄漏或者资源泄漏, 这个除非你有别的手段, 否则你用这个测试方
法, 对这个问题没有什么帮助.
对于我现在写的程序而言, 静悄悄地引发资源泄漏的危险, 和返回值没有检查的危
险是差不多的, 都是不可靠. 而返回值没有被正确处理更容易看出来.
代码质量分析工具我们也没有. 我妹妹在一个美国软件公司做, 他们用一个外部公
司的软件来分析代码质量, 对软件分析出的可疑点, 都要重点地做代码review.
莫华枫 写道:
今年的代码, C++ 只完成少量工作, 大部分的工作是 C 的, 偷懒了 :) ---- 反正
返回值方式都免不了.
看来, 屁股决定脑袋, 做的事情决定了思维的方式.
我眼下不是一个典型 C++ 用户, 发言无效 :)
莫华枫 写道:
> 保证使用raii管理资源,就无须担心资源泄漏。
>
呵呵。按照readsea的那种说法,我觉得就算不用异常一样的问题多多。异常不是万能。如果异常能够避免程序员所有的错误那么我们的程序 员日子就好过了。
但是,在异常和错误代码之间的选择。我还是倾向于在纯粹的c++环境中(不需要跨边界)还是异常好些。
如果我改了个指针。是否错误代码能够想外面提供一个此指针作废的提示了?即使有,我也会马上不用这个人写的代码。最安全的方式是在函数中恢 复调用前的状态。这个问题根本不关错误代码或者异常的问题---归根结底是一个编程素质的问题。工具能够帮你检查的是在你有源代码的情况下,没源代码你怎 么办?如果恰好你忘记check这个修改了指针的函数。。。。可想而知,,当程序崩溃的时候,你就也崩溃了。这个时候也许栈都被改得一塌糊涂了,离错误发 生地远了十万八千里。
在07-10-29,莫 华枫 <longsh...@gmail.com> 写道:
保 证使用raii管理资源,就无须担心资源泄漏。
1. 这玩意好难缠啊, 可能会在任何一个地方冒出来, 一个 = 都可能被打断
2. 代码倒是清晰了很多
3. 现在不用也不行啊, 这么多库都用它
那就用它吧, 尽量用得好点.
至于错误提醒之类, 老早老早写程序就会有一个窗口(即使是gui project, 程序开
始的时候也会自行创建一个 console 窗口), 发现什么重要的错误, 都即时
output 出来, 不用异常也能及早发现有问题的地方.
排除掉目前做的程序对我的影响, 之前我对异常的态度应该是:
1. 这玩意好难缠啊, 可能会在任何一个地方冒出来, 一个 = 都可能被打断
On 10/29/07, red...@gmail.com <red...@gmail.com> wrote:
排 除掉目前做的程序对我的影响, 之前我对异常的态度应该是:
1. 这玩意好难缠啊, 可能会在任何一个地方冒出来, 一个 = 都可能被打断
这.. 如果一个地方是会出错的,那么就是会出错的,不管用异常还是用错误代码,总是要面对的。用错误代码的话,"="就是一次函数调用,不仍然还是要check 错误返回值嘛。
讨论下来, 我才发现我有一个好玩的现象了: 如果我用 STL, 我就会倾向于用更多
的 C++ 特性, RAII, 异常等;
如果我要不用STL, 又要写底层的操作, 例如创建一个共享内存区, 创建一个FIFO,
我就会很自然地使用 C style.
> 这就限制了异常在你们项目中的应用异常。我想你们是没法用智能指针的吧。
智能指针的同步开销在用得多的对象中无法承受, 性能要求不高的地方, 可能又用
了 python 了, 所有智能指针真的用得少. 但是用过一个 "手工智能指针", 就是
自己 addref, release 控制生存期的指针, 那是一个多个线程都会访问的
session 型数据结构, 但是几个线程都有明确的开始使用和使用完毕的点; 每次复
制 addref, release 开销太高, 手工控制成本就低了.
> 从我的业务领域来看,没有你那么受限,所以用起异常来要容易得多。所以我并
> 没有觉得异常有多大的问题。
> 咱们是不是在瞎子摸象啊?:P
哈哈 :)
在 2007-10-26五的 19:32 +0800,pongba写道:
> 首先,我坚定的认为应该用异常,应该使用异常作为错误报告的唯一机制。哦,
> 实际上,不是我,是Brad Abrams(《.NET框架设计指南》作者),Anders(C#
> 之父),Andrei Alexandrescu(《C++ Coding Standard: 101 Rules》作者)
> 这么说的。
>
> 其次,国内的C++开发者估计都知道,在绝大多数C++项目中,异常都是被禁用
> 的,至于是不是连编译选项都关掉了还是只是在编码规定上禁止,就不知道了
> (你那边的实际情况是什么?:-)),据说就连Google内部也是禁用异常的。所
> 以估计情况还不仅仅局限于国内,甚至地球上的C++项目大抵都是如此。
>
> 然而不可否认的一个事实是,C++社群这么多年来的经验告诉我们,使用异常
> (具体参见《C++ Coding Standard: 101 Rules》第7x章~第7y章(x=?和y=?我
> 忘了))。Herb Sutter和Andrei Alexandrescu在C++异常方面的经验和造诣估
> 计C++社群还没有几个人能够质疑的。
>
> 生下来的问题就很简单了:为什么存在这个理想与现实的矛盾?这就是我想请大
> 家讨论的:P
>
> 个人一点看法:
>
> 1. Fallacies和Myths导致的Fear心态,进而导致在异常的正确实践方式方面的
> 标准缺乏(《C++ Coding Standard: 101 Rules》出版之前,你能找到一份哪怕
> 比较标准的,权威的,关于如何,何时使用异常的参考文献吗?)
>
> 2. 旧时代异常实现手段的落后(g++的影栈(shadow stack)实现)以及硬件的落
> 后(导致异常开销*相对*显得大),致使在当时,异常被打入冷宫。由于人在心
> 理上容易忘记一个论点的前提而记住其结论,所以若干年后的今天,仍然有大量
> 人认为异常的开销在任何情况下都很大很显著。而实际情况是,异常有开销,但
> 仅当在极度追求效率(时间&空间)的场合(极少),这个开销是完全可以忽略
> 的。
>
> 3. 教育问题。异常在C++里面是一个较新的特性。而且,使用错误代码,不需要
> 新学习语言特性,只要在现有语言特性(返回值)上hack就行了。不得不承认,
> 就连程序员也是害怕学新东西的。
>
> 4. 把错误处理的本质困难错误地归咎到异常身上。错误处理的本质困难是:定
> 义哪些情况属于错误,错误发生的时候在什么地点响应,错误发生如何确保操作
> 的完整语意(回滚、保存数据、释放资源)。无论用异常还是错误代码,这些问
> 题都同样尖锐。
>
> 5. 更多重量级的Fallacies:详见Raymond Chen(《The Old New Thing》作
> 者,Win32大牛)和Joel Spolsky(《Joel On Software》作者)的口水。
> Raymond Chen的"Cleaner, more elegant, and harder to recognize",和Joel
> 的两篇,这里和这里。要知道,这两位的口水的权重可不是一般的大啊。幸亏人
> 民群众的眼睛是雪亮的,Raymond和Joel的论点乍看十分、非常有理,实际经不
> 起推敲。你能看出里面的漏洞吗?
>
> 6. 遗留代码问题。遗留代码里面没有用异常,导致往遗留系统里面增加新模块
> 也首先与原先系统里用的错误处理策略的限制(这一点我不是很确定,有实际经
> 验的可以明确的举例说明一下?)
>
> 7. 心理问题。大量C++程序员有C背景(也许是因为教材是从C往上写的也许其他
> 什么原因),从心里就没有把异常当成一个first-class的,专门的,唯一的错
> 误处理机制。Java通过把人们逼上梁山解决了这个心理问题。而对于C++程序员
> 来说,也许下意识里面最"熟悉"的就是用返回值来报错;至于找出一堆理
> 由...well...只是找出一堆理由而已。能找出一堆经得起推敲的理由,那才是真
> 的理由。不过,对自己找的理由从来都不需要推敲的。
>
> 最想知道的就是大家经历的实际项目中的情况,越明确,越实际越好:-) 我似乎
> 把空话套话都给说完了:P
> --
> 刘未鹏(pongba)|C++的罗浮宫
> http://blog.csdn.net/pongba
> TopLanguage
> http://groups.google.com/group/pongba
> >
1) C++的异常算然形式看起来很自由,代价却是埋伏下了若干的陷井,唯一避免的
办法是背熟 More Effective C++ 那一整章 :)
2) RAII 在语言级支持得不够,反正是不方便。
3) 异常没有ABI规范,所以连 COM 也只能用返回值
On 10月26日, 下午7时32分, pongba <pon...@gmail.com> wrote:
> 首先,我坚定的认为应该用异常,应该使用异常作为错误报告的唯一机制。哦,实际上,不是我,是Brad
> Abrams(《.NET框架设计指南》作者),Anders(C#之父),Andrei Alexandrescu(《C++ Coding
> Standard: 101 Rules》作者)这么说的。
>
> 其次,国内的C++开发者估计都知道,在绝大多数C++项目中,异常都是被禁用的,至于是不是连编译选项都关掉了还是只是在编码规定上禁止,就不知道了(你那边的实际情况是什么?:-)),据说就连Google内部也是禁用异常的。所以估计情况还不仅仅局限于国内,甚至地球上的C++项目大抵都是如此。
>
> 然而不可否认的一个事实是,C++社群这么多年来的经验告诉我们,使用异常(具体参见《C++ Coding Standard: 101
> Rules》第7x章~第7y章(x=?和y=?我忘了))。Herb Sutter和Andrei
> Alexandrescu在C++异常方面的经验和造诣估计C++社群还没有几个人能够质疑的。
>
> 生下来的问题就很简单了:为什么存在这个理想与现实的矛盾?这就是我想请大家讨论的:P
>
> 个人一点看法:
>
> 1. Fallacies和Myths导致的Fear心态,进而导致在异常的正确实践方式方面的标准缺乏(《C++ Coding Standard: 101
> Rules》出版之前,你能找到一份哪怕比较标准的,权威的,关于如何,何时使用异常的参考文献吗?)
>
> 2. 旧时代异常实现手段的落后(g++的影栈(shadow
> stack)实现)以及硬件的落后(导致异常开销*相对*显得大),致使在当时,异常被打入冷宫。由于人在心理上容易忘记一个论点的前提而记住其结论,所以若干年后的今天,仍然有大量人认为异常的开销在任何情况下都很大很显著。而实际情况是,异常有开销,但仅当在极度追求效率(时间&空间)的场合(极少),这个开销是完全可以忽略的。
>
> 3.
> 教育问题。异常在C++里面是一个较新的特性。而且,使用错误代码,不需要新学习语言特性,只要在现有语言特性(返回值)上hack就行了。不得不承认,就连程序员也是害怕学新东西的。
>
> 4.
> 把错误处理的本质困难错误地归咎到异常身上。错误处理的本质困难是:定义哪些情况属于错误,错误发生的时候在什么地点响应,错误发生如何确保操作的完整语意(回滚、保存数据、释放资源)。无论用异常还是错误代码,这些问题都同样尖锐。
>
> 5. 更多重量级的Fallacies:详见Raymond Chen(《The Old New Thing》作者,Win32大牛)和Joel
> Spolsky(《Joel On Software》作者)的口水。Raymond Chen的"Cleaner, more elegant, and
> harder to recognize<http://blogs.msdn.com/oldnewthing/archive/2005/01/14/352949.aspx>
> ",和Joel的两篇,这里 <http://www.joelonsoftware.com/items/2003/10/13.html>和这里<http://www.joelonsoftware.com/items/2003/10/15.html>
> 。要知道,这两位的口水的权重可不是一般的大啊。幸亏人民群众的眼睛是雪亮的,Raymond和Joel的论点乍看十分、非常有理,实际经不起推敲。你能看出里面的漏洞吗?
>
> 6.
> 遗留代码问题。遗留代码里面没有用异常,导致往遗留系统里面增加新模块也首先与原先系统里用的错误处理策略的限制(这一点我不是很确定,有实际经验的可以明确的举例说明一下?)
>
> 7.
> 心理问题。大量C++程序员有C背景(也许是因为教材是从C往上写的也许其他什么原因),从心里就没有把异常当成一个first-class的,专门的,唯一的错误处理机制。Java通过把人们逼上梁山解决了这个心理问题。而对于C++程序员来说,也许下意识里面最"熟悉"的就是用返回值来报错;至于找出一堆理由...well...只是找出一堆理由而已。能找出一堆经得起推敲的理由,那才是真的理由。不过,对自己找的理由从来都不需要推敲的。
>
> 最想知道的就是大家经历的实际项目中的情况,越明确,越实际越好:-) 我似乎把空话套话都给说完了:P
莫华枫 写道:
> redsee啊,咱们争了那么多,我开始相信你对异常的态度是有道理的。倒不是我
> 要跳票,而是我觉得你的观点在你所面临的业务特点中是最恰当的。:)
> 从以往的帖子里可以发现,你的业务偏重于实时,甚至有些类似嵌入。这种地方
> 对于C++而言不很擅长(相比C),或者说缺少可靠的证明。或许在这种领域中,
> 全功能的C++显得笨重,反而不如C来得实惠。所以,应用异常的时候,资源管理
> 比较麻烦。
我目前的多数项目, 内存资源充足(但是不能泄露), CPU 能力有时候比较紧张.
但是异常的开销这个问题, 其实不是主要问题, 我不会去用一个 getByKey() 不存
在就抛出异常的 hashmap 这种变态库, 异常还是可以当作发生得少的情况的.
讨论下来, 我才发现我有一个好玩的现象了: 如果我用 STL, 我就会倾向于用更多
的 C++ 特性, RAII, 异常等;
如果我要不用STL, 又要写底层的操作, 例如创建一个共享内存区, 创建一个FIFO,
我就会很自然地使用 C style.
> 这就限制了异常在你们项目中的应用异常。我想你们是没法用智能指针的吧。
智能指针的同步开销在用得多的对象中无法承受, 性能要求不高的地方, 可能又用
了 python 了, 所有智能指针真的用得少. 但是用过一个 "手工智能指针", 就是
自己 addref, release 控制生存期的指针, 那是一个多个线程都会访问的
session 型数据结构, 但是几个线程都有明确的开始使用和使用完毕的点; 每次复
制 addref, release 开销太高, 手工控制成本就低了.
> 从我的业务领域来看,没有你那么受限,所以用起异常来要容易得多。所以我并
> 没有觉得异常有多大的问题。
> 咱们是不是在瞎子摸象啊?:P
哈哈 :)
一个函数, 如果我觉得他碰到的错误, 高层很有可能可以修复的, 我多半会选择用
错误值来返回, 如果他碰到的错误我觉得很严重, 多半是修复不了的, 我就会用异
常; 处于两种之间, 难以判断的, 我会倾向于用错误值.
而且, D 的 scope 不仅是对一场处理有帮助, 对错误值的处理同样也是有帮助的,
可以在函数级别的 scope(exit) 中做释放处理, 这样, 判断出错误的代码, 就可
以立即 return 错误值.
前天看 tango 的 hashmap, 刚开始看漏了, 以为 get (bykey) 的那个函数, 只有
一个版本, 就是找不到 key 就抛异常, 当时心里就想骂娘了, 这不是要让我自己
copy 再改一份吗? 这里的代码, 每秒钟十万个查询, 大多数都是找不到的.
然后同事提醒我, 下面还有一个版本, 是错误值返回的, 心里立刻给tango 库作者
道歉: 别人还不是那么笨的. ----- 我觉得写基础库的时候, 这种会影响到性能的
api, 两个版本都提供比较好; 库的用户更加知道, 用错误值更好, 还是用异常更
好. 如果只提供一个版本, 我则宁愿要错误值的, 需要的时候再转异常, 没有不能
接受的开销.
莫华枫 写道:
> 再问redsea一个问题,如果用D了,你会用异常吗?
>
OT: redsea 用的 scim吧?这东东老是把“一场”放在“异常”前面,呵呵。
在 2007-10-30二的 11:22 +0800,red...@gmail.com写道:
OT: redsea 用的 scim吧?这东东老是把“一场”放在“异常”前面,呵呵。
在 2007-10-30二的 11:22 +0800,red...@gmail.com写道:
哈, 之前用 linux desktop 的时候, 倒是用 scim 的, 现在用 windows desktop,
用的是google 拼音, yichang 输入了许多次, 现在总算在前面了, 其实他应该可
以分析出不同的用户的行业, 提供不同的次序的.
用回 windows desktop 几个主要原因是:
1. source insight, linux 下的 source navigator 还是太差
2. windows 跑在虚拟机里面的速度太慢了, 但是经常碰到需要 IE 检查 web
page, 所以还是 windows host, vmware 跑linux 好些.
只要测试完整,案例充分,那么,即便正确没有被正确捕捉,也能够立刻反应出来(程序退出或崩溃)。但是error code如果没有被正确捕捉,除非检测所有相关状态和结果,否则无法发现。
为什么会认为"水平一般"的程序员无法胜任异常处理?java、C#程序员在平均编程能力上无法与C++的程序员相比,但他们依然能够应付异常,只是处理的好坏而已(异常保证)。
在C++里保证异常安全相对困难些,但是只要遵照pongba给出的三条半准则,(没有比这更简单了吧),依然可以很容易地使用异常。(请注意,其中第三条是为了获得强烈保证,如只需获得基本保证,也就是error code一般情况下所做的,只需前头两条半即可,比error code所需的要求更少)。
同时,异常使得软件的结构更清晰,更接近于业务模型。我观察下来,很多初入行的程序员(我身边的都是C#),很自然地接受了异常,在处理异常的时候,也没有任何困难。C++新手或许难一些,但是只要记住pongba的三条半(至少两条半),也就不在话下了。
即便是一个团队中都是习惯于老方式的C++程序员,那么推广和使用异常也不是件难事,毕竟都已经是老鸟了。关键在于观念。请记住,一个团队永远不可能"自己"形成足够的能力,团队是需要建设的。
在07-10-28,wang xin <cber.w...@gmail.com > 写道:测试是在编码之后进行的,使用异常带来的结果就是编码过程中的难度变大,而用error code就没有这个问题了,即便是水平一般的程序员,也可以很简单的把程序给调试通过并提交给测试用例
如果编码结束后,使用异常当然就没有什么了,何况之后使用异常比用error code还好在07-10-28,yq chen <mephist...@gmail.com> 写道:话也可以反过来说:有了充分的测试,使用异常又如何?关键是有没有充分的测试?
在07-10-27,wang xin <cber.w...@gmail.com > 写道:对,有了充分的测试,即便不用异常又如何
"异常",顾名思义,就是不正常的情况,unhappy path在一个程序的运行中出现的概率实在是太低了(当然,如果频率高的话,就可以用一条happy path来取代它);再说了,又不是所有的程序都要求24 X 7,像我们开发的那个server程序,即便用户高达4xxx家(不乏一些要求很苛刻的用户,如:美国海军、DoD等……),依然有若干fatal error运行程序崩溃。
在07-10-27,oldrev < old...@gmail.com > 写道:认为用返回值比异常能够更好地保证错误安全其实和认为用静态语言比动态语言开
发的程序质量高一样是不可信的,唯一可信的是测试。--
Everything is possible and available if we trust ourselves!
从诸多理论文章来看,异常确实比返回码好。返回码不仅仅是占用了本来有意义返回的返回值,而且,混淆了错误和返回值。本质上, 返回值是可以忽略的,而错误不能。异常恰好开辟了一个新的信道返回错误,并且不可忽略。但是,什么是错误,什么是返回值是不能脱离设计意图来谈的,但我们 常常并不提及这一点。从而认为,让返回值返回错误代码和普通的结果从形式上来说,是一样的,但这种思路这将导致接口定义的含糊。
从实践上来说,要想正确运用异常,首先是离不开contract.异常的抛出条件可以通过契约来形式化的定义, 从而避免了凭经验决定是否抛异常。其次是强保证。强保证是事务性的,只有强保证,才可能让异常安全穿越而不必捕获不关心的异常, 对于template,通常还可以选择异常中立。这一点常常被忽略。基本保证基本无用,除非可以在更高层次全部撤销其效果, 但这种做法不大可能比返回值方式更简单和高效。事实上,即便考虑到效率因素,基本上也能做到强保证。从我的经验来看,接口定义不良导致无法实现强保证的概 率大的多。
异常问题说到底是和设计紧密相关的,错误码的设计不可能稍加修改或不加修改就用异常替换掉。正好昨天review代码,有个很好的例子。
一个uuid类的方法setByString,可以接受字符串,并以二进制方式存储在对象中.很显然,这里的字符串可能格式错误,从而无法处理。原来的设 计是,不正确的字符串就抛出异常。这样也确实可以工作。但是问题是,用户代码并没有uuid相关的知识,因此也无从判断字符串是否合法, setByString的happy-path就是处理字符串,却把非法字符串作为异常。因此,这种设计本身就不合理,而不是异常有问题。想象一下,编译 器把语法错误当作异常处理会怎样?
修改的建议有两种:
1. setByString返回bool值,成功返回true,失败返回false.这是用错误码来处理,这里很合适。
2. 提供一个validate方法用来验证字符串。setByString的pre_condition要求 validate(str_uuid)为真。用户就可以保证调用setByString的字符串一定合法。
这样,在setByString中可以去掉validate部分,可以运行的更快。而validate的责任交给用户代码。
从我的经历来看,说异常不好用,主要原因是思想还没转过弯来导致的滥用。其实返回码也一样有滥用问题,历史原因造成的大家忍受程度比较高而已。
在07-10-31,redsea (肖海彤) <red...@gmail.com > 写道:
这 个扩展看来有用, 什么 fopen 之类的加上, 再加上警告当错误的哪个编译选项, 想忽略都不行了.
在07-10-31,lijie <cpu...@gmail.com> 写道:刚 发现gcc有个括展, 可以对返回值没有检查予以警告。
从诸多理论文章来看,异常确实比返回码好。返回码不仅仅是占用了本来有意义返回的返回值,而且,混淆了错误和返回值。本质上,返回值是可以忽略的,而错误不能。异常恰好开辟了一个新的信道返回错误,并且不可忽略。但是,什么是错误,什么是返回值是不能脱离设计意图来谈的,但我们常常并不提及这一点。从而认为,让返回值返回错误代码和普通的结果从形式上来说,是一样的,但这种思路这将导致接口定义的含糊。
从实践上来说,要想正确运用异常,首先是离不开contract.异常的抛出条件可以通过契约来形式化的定义, 从而避免了凭经验决定是否抛异常。其次是强保证。强保证是事务性的,只有强保证,才可能让异常安全穿越而不必捕获不关心的异常, 对于template,通常还可以选择异常中立。这一点常常被忽略。基本保证基本无用,除非可以在更高层次全部撤销其效果, 但这种做法不大可能比返回值方式更简单和高效。事实上,即便考虑到效率因素,基本上也能做到强保证。从我的经验来看,接口定义不良导致无法实现强保证的概率大的多。
异常问题说到底是和设计紧密相关的,错误码的设计不可能稍加修改或不加修改就用异常替换掉。正好昨天review代码,有个很好的例子。
一个uuid类的方法setByString,可以接受字符串,并以二进制方式存储在对象中.很显然,这里的字符串可能格式错误,从而无法处理。原来的设计是,不正确的字符串就抛出异常。这样也确实可以工作。但是问题是,用户代码并没有uuid相关的知识,因此也无从判断字符串是否合法,setByString的happy-path就是处理字符串,却把非法字符串作为异常。因此,这种设计本身就不合理,而不是异常有问题。想象一下,编译器把语法错误当作异常处理会怎样?
修改的建议有两种:
1. setByString返回bool值,成功返回true,失败返回false.这是用错误码来处理,这里很合适。
2. 提供一个validate方法用来验证字符串。setByString的pre_condition要求 validate(str_uuid)为真。用户就可以保证调用setByString的字符串一定合法。
这样,在setByString中可以去掉validate部分,可以运行的更快。而validate的责任交给用户代码。
从我的经历来看,说异常不好用,主要原因是思想还没转过弯来导致的滥用。其实返回码也一样有滥用问题,历史原因造成的大家忍受程度比较高而已。
有道理.
再结合我的实践, 继续总结一下:
1. 一种特别一点的事情发生了, 只有上层才能知道, 这属于可预料的常规情况, 还是少见的错误情况, 如果是错误, 属于recoverable or unrecoverable 的, 也是上层才知道
2. 一个api, 如果其直接上层使用者很少, 那么特别一些的情况发生, 属于可预料的常规/还是错误, 很有可能编写本 api 的时候, 情况已经知道了, 统一一起进行设计应该效果更好
3. 一个api, 如果是个通用的 api, 各种上层用户代码很多, 那么无法预计别人怎么看自己返回的 "特别情况", 此时, 怎么选择? 我是这样做的: 对性能很有可能有影响的代码(调用次数多, "特别情况" 出现的几率不小), 肯定是用返回值; 否则就根据 "特别情况" 可能的特别性, 决定用返回值还是异常.
直接的说,我不会采用tryParse的方式,原因是tryParse和Parse不正交。这种"微妙"的差别非常糟糕, 还不如返回码方式。而validate和setByString是正交的。再考虑在C++中怎么支持pre_condition:
void setByString(string& str){
PRE_CONDITION(validate(str));
...
//process code.
}
很显然,这里的process code在try方式中有两份。如果你把这重复的代码放到单独的函数中,那和我的分解方式实际上就一样。功能分解的正交性是不可忽视的,tryParse完全可以基于validate和parse来定义(我这里说的是定义,不是实现),而validate和parse几乎不可能被混淆。这种正交分解的问题在于,validate和parse的协作方式还不能被很多程序员所接受。
2. 一个api, 如果其直接上层使用者很少, 那么特别一些的情况发生, 属于可预料的常规/还是错误, 很有可能编写本 api 的时候, 情况已经知道了, 统一一起进行设计应该效果更好
-------------------------------------------
基本上,我认为这是个错误的设计思路。设计API要做的是说清楚你定义的概念,尽量用"X是Y"的方式来定义的你接口,而不应该用"X是Y,不处理a,b,c"的方式。概念应该尽可能做到小而完备,套句老话,只做一件事,并且把它做到极致。
你永远无法知道用户会怎样使用你的API,所以,你不可能满足所有情况。现有的用户使用方式只是你的材料,可供你思考你的API的本质到底是什么,到底想要做什么,应该做什么。如果你只是致力于满足用户的欲望,失去的将是效率,得到的将是复杂性。你和用户最终都会陷入到扭曲抽象的泥坑中。
呵呵,这让我想起我们那些vb代码里随处可见的"on error resume next"。
在07-10-31,pongba <pon...@gmail.com > 写道:D&E里面提到,带resume的异常被大量实践证明不是一个好主意(虽然听上去很美)。
--
刘未鹏(pongba)|C++的罗浮宫
http://blog.csdn.net/pongba
TopLanguage
http://groups.google.com/group/pongba
On 10/31/07, wing fire <wing...@gmail.com> wrote:
2. 一个api, 如果其直接上层使用者很少, 那么特别一些的情况发生, 属于可预料的常规/还是错误, 很有可能编写本 api 的时候, 情况已经知道了, 统一一起进行设计应该效果更好
-------------------------------------------
基本上,我认为这是个错误的设计思路。设计API要做的是说清楚你定义的概念,尽量用"X是Y"的方式来定义的你接口,而不应该用"X是Y,不处理a, b,c"的方式。概念应该尽可能做到小而完备,套句老话,只做一件事,并且把它做到极致。
你永远无法知道用户会怎样使用你的API,所以,你不可能满足所有情况。现有的用户使用方式只是你的材料,可供你思考你的API的本质到底是什么,到底想 要做什么,应该做什么。如果你只是致力于满足用户的欲望,失去的将是效率,得到的将是复杂性。你和用户最终都会陷入到扭曲抽象的泥坑中。
说实话我没看懂redsea这第2点表达的意思.. :( 主要是最后一句话:"统一一起进行设计"不理 解。
这里异常的优点有:
1.可以比error code更清晰的表达类型
2.可以分出错误类型的大类别,可以统一处理,也可以特化
比如open不能打开可能有
1.权限不够
2.文件不存在
3.路径格式不合法
用error code表示不能如此清晰:)
在 07-10-31,red...@gmail.com<red...@gmail.com> 写道:
--
个人网站:http://zsp007.com.cn/
双学位:生物医学工程+计算机科学与技术
技能:C++(STL,BOOST) Python(Django) HTML+CSS AJAX
-- 张沈鹏
Python.Django的ORM中
Model.get是获取单一的查询返回值
当查询对象不存在,返回一个异常
该异常的类型为查询对象的类型.NotExist(比如A.NotExist),从ObjectNotExist继承而来
当查询对象多于一个,也返回一个异常
这里异常的优点有:
1.可以比error code更清晰的表达类型
2.可以分出错误类型的大类别,可以统一处理,也可以特化
比如open不能打开可能有
1.权限不够
2.文件不存在
3.路径格式不合法
用error code表示不能如此清晰:)