GotW #20 Code Complexity – Part I
著者:Herb Sutter
翻译:K ][ N G of @rk™
[声明]:本文内容取自www.gotw.ca网站上的Guru of the Week栏目,其著作权归原著者本人所有。译者kingofark在未经原著者本人同意的情况下翻译本文。本翻译内容仅供自学和参考用,请所有阅读过本文的人不要擅自转载、传播本翻译内容;下载本翻译内容的人请在阅读浏览后,立即删除其备份。译者kingofark对违反上述两条原则的人不负任何责任。特此声明。
Revision 1.0
Guru of the Week 条款20:代码的复杂性(第一部分)
难度:9 / 10
(本条款提出了一个有趣味的挑战:在一个简单得只有三行代码的函数里可以有多少条执行路经?其答案几乎将肯定让你吃惊。)
[问题]
在没有任何其它附加信息的情况下,下列代码中可以有多少条执行路经?
String EvaluateSalaryAndReturnName( Employee e )
{
if( e.Title() == "CEO" || e.Salary() > 100000 )
{
cout << e.First() << " " << e.Last()
<< " is overpaid" << endl;
}
return e.First() + " " + e.Last();
}
[解答]
假设:
a) 忽略对函数参数求值时的不同顺序以及由析构函数(destructor)抛出的异常。[注1]
下面的问题提给无所畏惧的勇者:
如果允许析构函数抛出异常,那么共会有多少条执行路经呢?
b) 调用的函数被认为具有原子性。事实上,例如”e.Title()”这个调用就可能由于好几个原因而抛出异常(比如,它自己本身可能抛出异常;它也可能由于「未能捕获由其调用的另一个函数所抛出的异常」而抛出异常;或者它可能采用return by value(传值返回)方式从而造成临时对象得构造函数可能抛出异常)。这里我们假设对于函数而言,只关注执行e.Title()操作的结果,即完成该操作后是否抛出了异常。
解答:23(仅仅在4行代码里!)
如果你找到了 给自己评等级
---------------------------------------------------------------------
3 平均水平(Average)
4-14 能够认知异常(Exception-Aware)
15-23 精英资质(Guru Material)
这23条执行路径包括:
——3条与异常无关的(non-exceptional)路径
——20条暗藏的路径,都与异常有关
要理解那3条普通路径,诀窍就是要知道C/C++的“短路求值规则(Short-Circuit Evaluation Rule)”:
1. 如果e.Title()==”CEO”,那么就不需要对第二个条件求值了(比如,e.Salary()将不会被调用),但cout还是会被执行的。[注2]
2. 如果e.Title()!=”CEO”但e.Salary()>100000,那么两个条件都会被求值,cout也会被执行。
3. 如果e.Title()!=”CEO”且e.Salary()<=100000,那么cout将不会被执行。
下述都是由异常引出的执行路径:
String EvaluateSalaryAndReturnName( Employee e )
^*^ ^4^
4. 引数采用pass by value(值传递)方式,这将唤起Employee copy constructor。这个copy操作可能抛出异常。
*. 在将函数临时的返回值拷贝到函数调用者的区域时,String的copy constructor可能抛出异常。然而在这里我们忽略这种可能性,因为其是在函数外部发生的(何况从目前的情形来看,现有的执行路径已经够我们忙的了!)
if( e.Title() == "CEO" || e.Salary() > 100000 )
^5^ ^7^ ^6^ ^11^ ^8^ ^10^ ^9^
5. 成员函数Title()本身就可能抛出异常;或者其采用return by value方式返回class type的对象,从而导致拷贝操作可能抛出异常。
6. 为了与有效的operator==相匹配,字符串也许需要被转换成class type(或许与e.Title()的返回型别相同)的临时对象,而这个临时对象的构造过程可能抛出异常。
7. 如果operator==是由程序员提供的函数,那么它可能抛出异常。
8. 与#5类似,Salary()本身可能抛出异常,或者由于其返回临时对象而造成在临时对象的构造过程中抛出异常。
9. 与#6类似,可能需要构造临时对象,而这个构造过程可能抛出异常。
10. 与#7类似,这或许是由程序员提供的函数,那么它可能抛出异常。
11. 与#7和#10类似,这或许是由程序员提供的函数,那么它可能抛出异常。
cout << e.First() << " " << e.Last()
^17^ ^18^
<< " is overpaid" << endl;
12-16 如C++标准草案所述,这里的五个对operator<<的调用都可能抛出异常。
17-18 与#5类似。First()和/或Last()可能抛出异常,或者由于其返回临时对象而造成在对象的构造过程中可能抛出异常。
return e.First() + " " + e.Last();
^19^ ^22^^21^ ^23^ ^20^
19-20 与#5类似。First()和/或Last()可能抛出异常,或者由于其返回临时对象而造成在对象的构造过程中可能抛出异常。
21.与#6类似,可能需要构造临时对象,而这个构造过程可能抛出异常。
22-23 与#7类似,这或许是由程序员提供的函数,那么它可能抛出异常。
本期GotW条款的目的是演示「在一个允许异常机制的语言中,简单的代码里可以存在多少条暗藏的执行路径」。这种暗藏的复杂性会影响函数的可靠性和可测性吗?请在下一期GotW中寻找这个问题的答案。
[注1]:决不允许一个异常从析构函数中渗透出来。如果允许这样做,代码将无法正常工作。请看我在C++Report Nov/Dec 1997中有关的更多讨论:Destructors That Throw and Why They're Evil。
[注2]:如果对==、||和>予以正确恰当的重载(overload),那么在if语句中,||或许是一个函数调用。如果其是一个函数调用,那么“短路求值规则”会被抑住,这样if语句中的所有条件将总是被求值。
(完)