网站开发属于哪个大学专业,wordpress头部警告错误,湖南网站建设公司 尖端磐石网络,建小公司网站要多少钱More Effective C之异常Exceptions 条款12#xff1a;了解“抛出一个exception”与“传递一个参数 ”或“调用一个虚函数”之间的差异条款13#xff1a;以by reference方式捕捉exceptions条款14#xff1a;明智运用exception specifications条款15#xff1a;了解异常处理之异常Exceptions 条款12了解“抛出一个exception”与“传递一个参数 ”或“调用一个虚函数”之间的差异条款13以by reference方式捕捉exceptions条款14明智运用exception specifications条款15了解异常处理exception handling的成本 条款12了解“抛出一个exception”与“传递一个参数 ”或“调用一个虚函数”之间的差异 函数参数的声明语法和catch子句的声明语法简直如出一辙
class Widget { ... }; //某个class
void f1(Widget w); // 参数Widget
void f2(Widget w); // 参数Widget
void f3(const Widget w); // 参数const Widget
void f4(Widget* pw); // 参数Widget *
void f5(const Widget* pw); // 参数const Widget *catch (Widget w); ... // 捕捉类型Widget
catch (Widget w); ... // 捕捉类型Widget
catch (const Widget w); ... // 捕捉类型const Widget
catch (Widget* pw); ... // 捕捉类型Widget *
catch (const Widget* pw); ... // 捕捉类型const Widget *我们可能因而假设“从抛出端传出一个exception到catch子句”基本上和“从函数调用端传递一个自变量到函数参数”是一样的。其实有相同之处但是也有重大的不同点。 让我们从相同点开始谈起。函数参数和exception的传递方式有3种by valueby referenceby pointer。然而视所传递的参数或exceptions发生的事情可能完全不同。原因是当调用一个函数控制权最终会回到调用端除非函数失败以至于无法返回但是当抛出一个exception控制权不会再回到抛出端。试看以下函数不但传递一个Widget作为参数也抛出一个Widget exception
// 此函数从一个stream中读取一个Widget。
istream operator (istream s, Widget w);
void passAndThrowWidget() {Widget localWidget;cin localWidget; // 将localWidget以Widget传给operatorthrow localWidget; // 将localWidget抛出成为一个exception
}当localWidget被交到operator函数手中并没有发生复制copying行为。而是operator内的reference w被绑定与localWidget身上。此时对w所做的任何事情其实是施加于localWidget身上的。这和localWidget当做一个exception的情况不同。不论被捕捉的exception是以by value或by reference方式传递此处不可能以by pointer方式传递——那将造成类型不吻合都会发生localWidget的复制行为而交到catch子句手上的正是那个副本。一定是这样因为此情况下一旦控制权离开passAndThrowWidgetlocalWidget便离开了其生存空间scope)于是localWidget destructor会被调用。如果此时是以localWidget本身传递给一个catch子句的此子句收到的将是一个被析构的Widget一个已经仙逝的Widget。这具尸体曾经负载一个Widget但现在已经不是了没有作用了。这便是为什么C特别要声明一个对象被抛出作为exception时总是会发生复制copying。 即使被抛出的对象没有瓦解的危险复制行为还是会发生。例如假设passAndThrowWidget将localWidget声明为static
void passAndThrowWidget() {static Widget localWidget;cin localWidget; // 将localWidget以Widget传给operatorthrow localWidget; // 将localWidget抛出成为一个exception
}当上述函数抛出exception还是会产生一个localWidget副本。意味着即使此exception以by reference方式被捕捉catch端还是不可能修改localWidget只能修改localWidget的副本。“exception objects必定会造成复制行为”这一事实也解释了“传递参数”和“抛出exception”之间的另一个不同后者常常比前者慢。 当对象被复制当做一个exception复制行为是由对象的copy constructor执行的。这个copy constructor相应于该对象的“静态类型”而非“动态类型”。例如考虑下面这个稍加修改的passAndThrowWidget函数
class Widget { ... };
class SpecialWidget : public Widget { .. };
void passAndThrowWidget() {SpecialWidget localSpecialWidget;...Widget rw localSpecialWidget; // rw代表一个SpecialWidgetthrow rw; // 抛出一个类型为Widget的exception
}这里抛出的是一个Widget exception——虽然rw实际代表的是一个SpecialWidget。这是因为rw的静态类型是Widget而非SpecialWidget.rw虽然实际上代表一个此行为模式可能不是我们想要的但它和其他所有“C复制对象”的情况一致复制动作永远是以对象的静态类型为本。后续条款25会展示一种以对象的动态类型为本进行复制的技术 “exception对象是其他对象的副本”这个事实会对我们“如何在catch语句块内传播exceptions”带来冲击。考虑以下两个catch语句块乍见之下似乎做了相同的事情
catch Widget w) { // 捕捉Widget exception... // 处理exceptionthrow; // 重新抛出exception使它能继续传播
}
catch (Widge w) {...throw w; // 传播被捕捉的exception的一个副本
}这两个catch语句块之间唯一的差异就是前者重新抛出当前的exception后者抛出的是当前exception的副本。如果暂且排除“额外的复制行为所带来的性能成本”因素这两种做法有差别吗 有第一语句块重新抛出当前的exception不论其类型为何。更明确地说如果最初抛出的exception的类型是SpecialWidget第一语句块会传播一个SpecialWidget exception——甚至虽然w的静态类型是Widget。这是因为当此exception被重新抛出时并没有发生复制行为。第二catch语句块则抛出一个新的exception其类型总是Widget因为那是w的静态类型。一般而言我们必须使用以下语句 throw; 才能重新抛出当前的exception其间没有机会让我们改变被传播的exception的类型。此外它也比较有效率因为不需要产生新的exception object。 附带一提为exception所做的复制动作其结果是个临时对象。如条款19所言这给予编译器进行优化的权利。然后我们不能期望编译器在这上面有什么好表现Exception毕竟是比较罕见的所以即使编译器厂商在其优化上面灌注的心力不大也在意料之中。 让我们检验3种catch子句它们都有能力捕捉“被passAndThrowWidget抛出”的Widget exception
catch (Widget w) ... // 以by value的方式捕捉
catch (Widget w) ... // 以by reference的方式捕捉
catch (const Widget w) ... // 以by reference-to-const的方式捕捉我们立刻就注意到了“参数传递”和“exception传播”之间的另一个区别一个被抛出的对象如先前所解释必为临时对象可以简单地用by reference的方式捕捉不需要以by reference-to-const的方式捕捉。函数调用过程中将一个临时对象传递给一个non-const reference参数是不允许的但对exceptions则属合法。 然而让我们忽略这个差异回到“复制exception objects”的主题。我们知道如果以by value方式传递函数自变量便是对被传递对象做一个副本此副本存储于对应的函数参数中。如果by value方式传递exception亦发生相同的事情。因此当我们声明一个catch子句如下
catch (Widget w) ... // 以by value的方式捕捉。预期得付出“被抛出物”的“两个副本”的构造代价其中一个构造动作用于“任何exception都会产生的临时对象”身上另一个构造动作用于“将临时对象复制到w”。类似道理当我们以by reference方式捕捉一个exception
catch (Widget w) ... // 以by reference的方式捕捉
catch (const Widget w) ... // 以by reference-to-const的方式捕捉预期得付出“被抛出物”的“单一副本”的构造代价。这里的副本便是指临时对象。由于以by reference方式传递函数参数时并不发生复制行为所以“抛出exception”和“传递函数参数”相比前者会多构造一个“被抛出物”的副本并于稍后析构。 我们尚未讨论以by pointer方式抛出exceptions但throw by pointer事实上相当于pass by pointer两者都传递指针副本。必须特别注意的是千万不要抛出一个指向局部对象的指针因为该局部对象会在exception传离其scope译注控制权同事也离开scope时被销毁因此catch子句会获得一个指向“已被销毁的对象”的指针上。这正是“义务性复制copy规则”的设计要避免的情况。 “自变量传递”与“exception传播”俩动作有着互异的做法其中一个不同就是对象从“调用端或抛出端”被搬移到“参数或catch子句”时的做法如上所述第二个不同则是“调用者或抛出者”和“被调用者或捕捉者”之间所存在的类型吻合type match规则。试考虑标准程序库的数学函数sqrt
double sqrt(double); // from cmath or match.h
int i;
double sqrtOfi sqrt(i);其中没有什么值得大惊小怪的。C允许隐式转换将int转换为double所以在调用sqrt的过程中i会被默默地转换为一个double而sqrt的结果将应该是double。一般而言如此的转换并不发生于“exceptions与catch子句相匹配”的过程中。下面这段代码
void f(int value) {try {if (someFuction()) {throw value; // 抛出一个int }}catch (double d) { // 在这里处理类型为double的exception...}
}try语句块中抛出的int exception绝不会被“用来捕捉double exception”的catch子句捕捉到。后者只能捕捉类型确确实实为double的exceptions其间不会有类型转换的行为发生。所以如果int exception被捕捉它一定是被某些其他也许是外围catch子句捕捉的它们的捕捉类型一定是int或int或许再加上const或volatile之类的限定词。 “exceptions与catch子句相匹配”的过程中仅有两种转换可以发生。第一种是“继承架构中的类转换inheritance-based conversions”。是的一个针对base class exceptions而编写的catch子句可以处理类型为derived class的exceptions。例如C标准程序库中定义有exceptions集成体系其中的诊断diagnostics相关类如下 #mermaid-svg-SVWlSulnjyGH3xq3 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SVWlSulnjyGH3xq3 .error-icon{fill:#552222;}#mermaid-svg-SVWlSulnjyGH3xq3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SVWlSulnjyGH3xq3 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-SVWlSulnjyGH3xq3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SVWlSulnjyGH3xq3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SVWlSulnjyGH3xq3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SVWlSulnjyGH3xq3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SVWlSulnjyGH3xq3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SVWlSulnjyGH3xq3 .marker.cross{stroke:#333333;}#mermaid-svg-SVWlSulnjyGH3xq3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SVWlSulnjyGH3xq3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SVWlSulnjyGH3xq3 .cluster-label text{fill:#333;}#mermaid-svg-SVWlSulnjyGH3xq3 .cluster-label span{color:#333;}#mermaid-svg-SVWlSulnjyGH3xq3 .label text,#mermaid-svg-SVWlSulnjyGH3xq3 span{fill:#333;color:#333;}#mermaid-svg-SVWlSulnjyGH3xq3 .node rect,#mermaid-svg-SVWlSulnjyGH3xq3 .node circle,#mermaid-svg-SVWlSulnjyGH3xq3 .node ellipse,#mermaid-svg-SVWlSulnjyGH3xq3 .node polygon,#mermaid-svg-SVWlSulnjyGH3xq3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SVWlSulnjyGH3xq3 .node .label{text-align:center;}#mermaid-svg-SVWlSulnjyGH3xq3 .node.clickable{cursor:pointer;}#mermaid-svg-SVWlSulnjyGH3xq3 .arrowheadPath{fill:#333333;}#mermaid-svg-SVWlSulnjyGH3xq3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SVWlSulnjyGH3xq3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SVWlSulnjyGH3xq3 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-SVWlSulnjyGH3xq3 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-SVWlSulnjyGH3xq3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SVWlSulnjyGH3xq3 .cluster text{fill:#333;}#mermaid-svg-SVWlSulnjyGH3xq3 .cluster span{color:#333;}#mermaid-svg-SVWlSulnjyGH3xq3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SVWlSulnjyGH3xq3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} exception logic_error runtime_error domain_error length_error range_error overflow_error invlaid_argument out_of_range underflow_error 一个针对runtime_error而编写的catch子句可以捕捉类型为range_error、overflow_error及underflow_error的exceptions。一个可接受最根源类exception的catch子句可以捕捉此继承体系下的所有exceptions。 此所谓的“集成架构中的exception转换”规则可适用于by valueby reference及by pointer3种形式:
catch (runtime_error) ... // 可捕捉类型为runtime_error、overflow_error及underflow_error的错误
catch (runtime_error) ... // 同上
catch (const runtime_error) ... // 同上
catch (runtime_error*) ... // 可捕捉类型为runtime_error*、overflow_error*及underflow_error*的错误
catch (const runtime_error*) ... // 同上第二个允许发生的转换是从一个“有型指针”转换为“无型指针”所以一个针对const void*指针而设计的catch子句可捕捉任何指针类型的exception
catch (const void *) ... // 可捕捉任何指针类型的exception“传递参数”和“传递exception”的最后一个不同是catch子句总是依出现顺序做匹配尝试。因此当try语句块分别有针对base class而设计和针对derived class设计的catch子句一个derived class exception仍有可能被“针对base class 而设计的catch子句”处理掉。例如
try {...
}
catch (logic_error ex) { // 此语句块捕捉所有logic_error exceptions,同时包括domain_error,invalid_argument,length_error,out_of_range exceptions...
}
catch (invalid_argument ex { // 此语句绝不会执行起来因为所有的invalid_argument exceptions都会被上述子句捕捉...
}将此行为拿来和“调用虚函数时所发生的事情”比对。当我们调用虚函数被调用的函数是“被调用某个对象的动态类型”中的函数。可以说虚函数采用所谓的“best fit”最佳吻合策略而exception处理机制遵循所谓的“first fit”最先吻合策略。如果“针对derived class而设计的catch子句”出现在“针对base case而设计的catch子句”之后编译器可能会给出一个警告——有些更严厉的编译器甚至会发出错误信息因为这样的代码在C中通常是不正确的。但是我们的最佳行动纲领就是先发制人绝不要将“针对base class而设计的catch子句”放在“针对derived class而设计的catch子句”之前。上述代码应该重新安排如下
try {...
}
catch (invalid_argument ex { // 处理invalid_argument exceptions...
}
catch (logic_error ex) { // 这里用来处理其他所有logic_error exceptions...
}因此我们可以说“传递对象到函数去或是以对象调用虚函数”和“将对象抛出成为一个exception”之间有3个主要差异。第一exception objects总是被复制如果by value方式捕捉它们甚至被复制两次。至于传递给函数参数的对象则不一定得复制。第二“被抛出成为exceptions”的对象其被允许的类型转换动作比“被传递到函数去”的对象少。第三catch子句一起“出现源代码的顺序”被编译器依次检验比对。其中第一个匹配成功便执行而当我们以某对象调用一个虚函数被选中执行的是那个“与对象类型最佳吻合”的函数不论它是不是源代码所列的第一个。 学习心得 exception在被抛出时必然会产生临时副本对象为了在传播过程中保有exception的原有类型不被复制为basic exception 使用throw语句不要使用throw ex语句catch子句的处理采用“first fit”处理方式所以如果有多个catch存在的情况下基类总是放在代码的后面。存在继承关系的两个Exception对象永远保持继承类derived class出现在基类base class的前面。
条款13以by reference方式捕捉exceptions 写一个catch子句时我们必须指明exception objects如何被传递到这个子句来。就像我们可以选择参数如何被传递到函数来一样现在我们也有3种选择by pointerby value或是by reference。 首先让我们考虑by pointer。理论上将一个exception从抛出端搬移到捕捉端必然是个缓慢的过程而by pointer应该是最有效率的一种做法因为by pointer是唯一在搬移“异常相关信息”是不需要复制对象的一种做法只需要复制一个指针。例如
catch exception { ... };
void someFunctiion() {static exception ex:...throw ex;...
}
void doSomething() {try {someFunction(); // 可能抛出exception *只复制exception*}catch (exceptiion *ex) { // 捕捉到exception*没有exception对象被复制...}
}这看起来十分优雅整齐但是它并不像我们所看到的那么好。为了让这段代码能够运行程序员必须有办法让exception objects在控制权离开那个“抛出指针”的函数之后依然存在。Global对象及static对象都没问题但程序员很容易忘记这项约束。于是常常写出这样的代码
void someFunctiion() {exception ex: // 局部的exception object将在此函数结束时销毁...throw ex; // 抛出一个指针指向即将被销毁的对象 ...
}这是很糟糕的情况因为catch子句所收到的指针指向不复存在的对象。 另一种做法是抛出一个指针指向一个新的heap object:
void someFunctiion() {...throw new exception; // 抛出一个指针指向一个新的heap-based object ...
}这避免了“捕捉到一个指针它却指向一个已不存在的对象”的问题。但是现在catch子句的作者遭遇了一个更难缠的问题应该删除获得的指针吗如果exception objec被分配于heap必须删除。否则便会泄露资源。如果exception object不是被分配于heap就不必删除之否则便会招致未受定义的程序行为。该怎么做才好呢 没有人知道该怎么做才好。某些人可能会把一个global或static对象的地址传出去另一些人可能会把一个位于heap中的exception object的地址传出去。Catch by pointer于是有了哈姆雷特的难题to delete or not to delete?这个问题没有答案。 此外catch-by-pointer和语言本身建立起来的惯例有所矛盾。4个标准的exception——bad_alloc(当operator new无法满足内存需求时抛出)、bad_cast当对一个reference施行dynamic_cast失败时抛出、bad_typeid当dynamic_cast被施行于一个null指针时抛出、bad_exception适用于未预期的异常情况——统统都是对象不是对象指针。所以我们无论如何必须以by value或by reference的方式捕捉它们。 Catch-by-value可以消除上述“exception是否需要删除”及“与标准exception不一致”等问题。然而在此情况下每当exception objects被抛出就得复制两次【见条款12】。此外它也会引起切割(slicing)问题因为derived class exception objects被捕捉被视为base class exception者将失去其派生成分。如此被切割过的对象其实就是base class objects它们缺少derived class data members当虚函数在其上被调用时会被解析为base class的虚函数这和对象以by value方式传递给函数时所发生的事情一样。例如考虑一个应用程序采用exception class标准继承体系的一个扩充版本
class exception {
public :virtual const char* what() throw();
};
class runtime_error : public exception { ... };
class Validation_error : public runtime_error {
public:virtual const char * what() throw();...
};
void someFunction() {...if (a validation test fails) {throw Validation_error();}...
}
void doSomething() {try {someFunction(); // 可能会抛出一个Validation_error object}catch (exception ex) { // 使用基类进行捕获cerr ex.what() ; // 调用的是exception::what()而不是Validation_error::what()...}
}被调用的what函数时base class版——即使被抛出的exception属于Validation_error类型而Validation_error重新定义了虚函数what。这种切割(slicing)行为几乎不是我们所要的。 剩下的就是catch-by-reference喽Catch-by-referece不需要蒙受我们所讨论的任何问题。它不像catch-by-pointer,不会发生对象删除问题因此也就不难捕捉标准的exception。它和catch-by-value也不同所以没有切割slicing问题而且exception objects只会被复制一次。 如果我们使用catch-by-reference重写上一个例子结果如下
void someFunction() {...if (a validation test fails) {throw Validation_error();}...
}
void doSomething() {try {someFunction(); // 可能会抛出一个Validation_error object}catch (exception ex) { // 这里改用catch by reference取代catch by valuecerr ex.what() ; // 调用的是Validation_error::what()而非exception::what()...}
}抛出端没有任何改变catch子句内的唯一改变是增加了一个符号。然后这个微小的修改成就了极大的不同catch语句块内所调用的虚函数调用的是我们所期望的Validation_error中的函数会被调用——如果他们重新定义了exception内的虚函数的话。 多么令人开心的结果呀如果catch by reference我们就可以避开对象被删除的问题相对于catch by pointer我们也可以避开exception objects的切割slicing问题相对于catch by value我们可以保留捕捉标准exception的能力我们也约束了exception objects需被复制的次数。 学习心得 异常捕获的三种方式catch-by-pointercatch-by-valuecatch-by-reference。尽管catch-by-pointer是抛出捕获方式性能最高的但是由于指针是否释放以及与标准exception抛出方式不匹配所以不推荐使用。catch-by-value有性能不佳至少被拷贝两次同时存在切割slicing丢失了derived class信息的缺点也不推荐使用。剩下catch-by-reference, 性能相对可接受只拷贝一次与标准exception兼容同时保留了derived class的信息可实现多态polymorphism所以catch-by-reference成为了最被推荐的捕获方式。
条款14明智运用exception specifications exception specification还是有吸引力的。它让代码更容易被理解因为它明确指出一个函数可以抛出什么样的exceptions但它不只是一个漂亮的注释而已。编译器有时候能够在编译期间侦测到与exception specifications不一致的行为。如果函数抛出了一个并未列于其exception specification的exception这个错误会在运行期被检查出来于是特殊函数unexpected会被自动调用。可以说exception specifications不但是一种文档式的辅助也是一种实践式的机制用来规范exception的运用。它确实满吸引人的。 然而就像日常所见美貌只是一种肤浅的表现。unexpected的默认行为是调用terminate而terminate的默认行为是调用abort所以程序如果违反exception specification默认结果就是程序中止。是的局部变量不会获得销毁的机会因为abort会使程序停摆没机会执行此类清理工作。一个未获得尊重的exception specification就像大洪水一般会带来毁灭。最好永远不要发生这种事情。 不幸的是很容易就能写出一个函数让可怕的事情发生。因为编译器只会对exception specifications做局部性检验。它们所没有检验的——同时也是C明定不准拒绝的——是调用某个函数而该函数可能违反调用端函数本省的exception specification有些比较严谨的编译器会对此发生警告。 考虑以下的f1函数声明它没有exception specification。此函数可以抛出任何一种exception
extern void f1(); // 可以抛出任何东西在考虑函数f2它通过exception specification声称只抛出类型为int的exceptions:
void f2() throw(int);在C中f2调用f1绝对合法即使f1可能抛出一个exception而该exception违反了f2的exception specification
void f2() throw(int) {...f1(); // 合法甚至即使f1可能抛出int以外的exceptions。...
}这种弹性是必要的。如果带有exception specification的新代码和缺乏exception specification的旧代码要整合在一起的话这种弹性就有必要。 由于编译器心甘情愿地让我们调用“可能违反当前函数本身的exception specification”的函数并且由于如此的调用行为可能导致程序被迫中止所以如何才能将这种不一致性降到最低是很重要的思考。一个好办法就是避免将exception specification放在“需要类型自变量”的template身上。考虑下面这个template它看起来好像绝不会抛出任何exceptions
// 一个不良的template设计因为它带有exception specifications。
template class T
bool operator (const T lhs, const T rhs) throw() {return lhs rhs;
}这个template为所有类型定义一个operator函数针对同型的两个对象如果其地址相同便返回true否则返回false。 上述template有一个exception specification。指明被此template所产生出来的函数不会抛出任何exceptions。但其实那并非绝对为真因为有可能operator取址操作符已经被某些类型重载了。如果真是这样operator内部调用operator时便有可能由operator抛出一个exception。果真如此上述的exception specification便遭违反导致迈向unexpected之路。 这只是个特殊的例子。更一般化的问题是没有任何方法可以知道一个template的类型参数可能抛出什么exceptions。所以千万不要为template提供意味深长的exception specification因为templates几乎必然会以某种方式使用其类型参数。结论是不应该将templates和exception specification混合使用。 避免踏上unexpected之路的第二个技术是如果A函数内调用了B函数而B函数无exception specification那么A函数本身也不要设定exception specification。这是很简单的常识但是有一种情况很容易被忘记就是当允许用户注册所谓的callback(回调)函数时
// 函数指针类型用于窗口系统的callback回调函数——当窗口系统发出一个event。
typedef void (*CallBackPtr)(int eventXLocation, int eventYLocation, void *dataToPassBack);
// 窗口系统内的class用来放置用户注册的callback函数。
class CallBack {
public:CallBack(CallBack fPtr, void *dataToPassBack) :func(fPtr), data(dataToPassBack) {}void makeCallBack(int eventXLocation, int eventYLoaction) const throw();
private:CallBackPtr func; // callback发生时所要调用的函数。void * data; // 用来传给callback函数的数据。
};
// 为了实现callback我们调用已经给您注册的函数
// 并以“event的发生坐标”和“经过注册的数据”作为自变量
void CallBack::makeCallBack(int eventXLocation, int eventYLocation) const throw() {func(eventXLocation, eventYLocation, data);
}此处makeCallBack函数内部对func的调用有可能违反exception specification因为没有任何方法可以知道func可能抛出什么样的exception。 如果CallBackPtr typedef中加上exception specification就可以消除这个问题1
typedef void (*CallBackPtr)(int eventXLocation, int eventYLocation, void *dataToPassBack) throw();有了这个typedef现在如果注册一个callback函数而后者无法保证不抛出任何exception便会出现错误
// 一个不带有exception specification的callback函数。
void callBackFcn1(int eventXLocation, int eventYLocation, void* dataToPassBack);
void *callBackData;
...
CallBack c1(callBackFcn1, callBackData); // 错误因为callBackFcn1可能会抛出exception。
// 一个带有exception specification的callback函数。
void callBackFcn2(int eventXLocation, int eventYLocation, void* dataToPassBack) throw;
CallBack c2(callBackFcn2, callBackData); // 没问题因为callBackFcn2满足exception specification。避免踏上unexpected之路的第三个技术是处理“系统”可能抛出的exceptions。其中最常见的就是bad_alloc,那是在内存分配失败时由operator new和operator new[]抛出的。如果我们在函数内使用new operator我们必须有心理准备这个函数可能会遭遇bad_alloc exceptions. 虽说防微杜渐预防胜于治疗但有时候预防很困难治疗反倒简单。也就是有时候直接处理非预期的exceptions反而比事先预防来得简单得多。举个例子如果我们写的软件大量使用exception specifications但被迫调用程序库提供的函数而后者没有使用exception specification那么想要阻止非预期的exception发生就是件不切实际的事因为那非得改变程序库源代码不可。 如果“阻止非预期的exception发生”是件不切实际的事我们可以利用一个事实C允许以不同类型的exception取代非预期的exception。举个例子假设我们希望所有的非预期的exception都以UnexpectedException objects取而代之可以这么做
class UnexpectedExpection {}; // 所有非预期的exception objects都将被取代为此类objects
void convertUnexpected() {throw UnexpectedExpection();
}
// 并以convertUnexpected取代默认unexpected函数
set_unexpected(convertUnexpected);一旦完成这些布置任何非预期的exception便会导致convertUnexpected被调用于是非预期的exception被一个新的类型为UnexpectedException的exception取而代之。那么只要被违反的exception specification内含有UnexpectedExceptionexception的传播便会继续下去好似exception specification获得满足似的。但如果exception specification未包含UnexpectedExceptionterminate会被调用犹如从未取代unexpected一样。 “将非预期的exception转换为一个已知类型”的另一个做法就是依赖以下事实如果非预期函数的替代者重新抛出当前的exception该exception会被标准类型bad_exception取而代之。 以下代码就会发生这样的事情
// 如果非预期的exception被抛出此函数便被调用它只是重新抛出当前的exception
void convertUnexpected() {throw ;
}
// 设置convertUnexpected作为unexpected函数的替代品
set_unexpected(convertUnexpected);如果做了上述安排并且每一个exception specification都含有bad_exception(或其基类也就是标准类exception我们就再也不必担心程序会遇上非预期的exception时中止执行。任何非预期的exception都会被一个bad_exception取代而该exception会取代原来的exception继续传播下去。 现在我们了解到了exception specifications可能带来许多麻烦。编译器只为它执行局部性检查在templates中使用它会有问题它们很容易被不经意地违反而且当他们被违反默认情况下会导致程序草草中止。Exception specifications还有另一个缺点那就是它们会造成“当一个较高层次的调用者已经准备好要处理发生的exception时unexpected函数却被调用”的现象。例如考虑以下这段几乎从条款11抄录过来的代码
class Session {
public:Session();~Session();...
private:static void logDestruction(Session* objAddr) throw();
};
Session::~Session() {try {logDestruction(this);}catch (...) { }
}其中的Session destructor调用logDestruction以记录“有个Session object正被销毁”的事实并以catch ( … ) 明确指出它要捕获任何可能被logDestruction抛出的exception。然后logDestruction带有一个exception specification保证不抛出任何exceptions。现在假设某些被logDestruction调用函数抛出一个exception而logDestruction没有拦下它。当这个非预期的exception传播到达logDestruction函数unexpected会被调用默认情况下会导致程序的中止。这是正确的行为毫无疑问但这是Session destructor的作者真正想要的行为吗那位作者费尽苦心地处理所有可能的exceptions所以如果在中止程序之前没有给Session destructor内的catch语句块一个机会似乎并不公平。如果logDestruction没有设定exception specification则catch (…)子句会发挥效果。阻止它的办法之一就是将unexpected函数重新进行设定。重新生成新的Exceptionor通过throw将当前exception转换为bad_exception。 对exception specification保有持平的观点至为重要。它们对于“函数希望抛出什么样的exception”提供了卓越的说明。而在“违反exception specification的下场十分悲惨以至于需要立刻结束程序”的形势下它们提供了默认行为。但是虽然有这些好处它们相对有一些缺点包括编译器只对它们做局部性检验、很容易被不经意地违反等。此外它们可能妨碍更上层的exception处理函数处理未预期的exceptions——即使更上层的处理函数已经知道该怎么做。exception specification是一把双刃剑在将它加入函数之前请考虑它所带来的程序行为是否真是我们想要的。 学习心得 本条款告诉我们当针对函数声明的exception specification与函数实际抛出的exception不一致可能导致不符合预期的行为比如程序中止、上层代码无法捕获异常等问题。鉴于这个问题本条款建议我们1在不清楚函数实际会抛出什么异常的情况下宁可不使用exception specification特别是不要与template class混用2通过使用接口set_unexcepted重新配置抛出异常的行为。
条款15了解异常处理exception handling的成本 为了能够在运行时处理exception程序必须做大量的簿记工作。在每一个执行点它们必须确认“如果发生exception哪些对象需要析构”它们必须在每个try语句块的进入点和离开店做记号针对每个try语句块它们必须记录对应的catch子句及能够处理的exception类型。这些簿记工作必须付出代价。运行时期的比对工作以确保符合exception specification不是免费的exception被抛出时销毁适当对象并找出正确的catch子句也不是免费的。exception的处理需要成本即使我们从未使用关键字trythrow或catch我们也必须付出某些成本。 让我们从“即使从未使用任何exception处理机制也必须付出”的最低消费额谈起。我们必须付出一些空间放置某些数据结构记录着哪些对象已被完全构造妥当见条款10我们必须付出一些时间,随时保持那些数据结构的正确性。这些成本通常相当适当。尽管如此编译过程中如果没有加上对exception的支持程序通常比较小执行时也比较快。如果编译过程中加上对exceptions的支持程序就比较大执行时也比较慢。 理论上面对这些成本我们别无选择exceptions是C的一部分编译器必须支持它们就是这么回事 。即使我们未使用exception机制我们也不能期望编译器厂商消除这些成本因为程序通常是由多个独立完成的object files构成其中一个不做任何与exception相关的事并不表示其他也都如此。此外即使程序赖以构成的object files中没有任何一个运用了exceptions它们所链接link的程序库又如何只要程序的任何一部分运用了exceptions整个程序就必须支持它否则就不可能再运行时期提供正确的exception处理行为。 这只是理论。事实上大部分对exception处理机制有所支持的厂商允许我们自行决定是否要在他们所产生的代码上放置“exception支持能力”。如果我们知道程序没有任何一处使用trythrow或catch而且也知道所链接的程序库没有一个用到trythrow或catch我们可以在编译过程中放弃支持exception并因而免除了大小和速度的成本否则我们会获得一个其实并未使用的性质。随着时间过去程序库对exception的运用普及度愈来愈高这个策略会变得比较无处着力但是目前C软件开发的现况是如果我们决定不使用exceptions并让编译器知道编译器可以适度完成某种性能优化。对于避免exception的程序库而言这也是一个诱人的优化机会前提是必须保证“client端抛出的exceptions绝不会传入程序库”。这是一个困难的保证因为它会对“client重新定义程序库内的虚函数”带来妨碍也会对“client定制callback函数”带来排挤影响。 Exception处理机制带来的第二种成本来自try语句块。只要我们用上那么一个也就是说一旦决定捕捉exceptions就得付出那样的成本。不同编译器以不同的方法实现try语句块所付出的代价各不相同。粗略估计如果使用try语句块代码大约整体膨胀5%-10%执行速度亦大约下降这个数。这是在假设没有任何exceptions被抛出的情况下。此处我们所讨论的知识“代码中出现try语句块”的成本而已。为了将此成本最小化我们应该比避免必要的try语句块。 面对exception specifications编译器产出的代码倾向类似于面对try语句块的作为所以exception specification通常会招致与try语句块相同的成本。 这把我们带到一个中心思维抛出一个exception成本几何事实上这不应该成为关心的焦点因为exception应该是罕见的毕竟它们是用来表现异常的发生。80-20法则告诉我们如此的时间应该不会对一个程序的整体性能有太巨大的冲击才是。尽管如此如果抛出一个exception会带来多大的冲击答案是可能十分巨大。和正常的函数返回动作比较由于抛出exception而导致的函数返回器速度可能比正常情况慢3个数量级。这可是大冲击。但是只有在抛出exception时才需要承受这样的冲击。而exception的出现应该是罕见的。然后如果我们视exceptions为一种用来表现“相对平常”的状态的工具例如用来表现一个数据结构的遍历完成或是一个循环的结束那么现在正是好好反省的时机。 读者可能对抛出异常的成本这么大存疑可以通过使用自己的编译器进行测试验证 深思明辨的做法就是了解本条款所描述的成本但是不要对以上数据过度敏感。不论exception处理过程需要多少成本我们都不应该付出比该付出的部分更多。为了让exception的相关成本最小化只要能够不支持exceptions编译器便不支持请将对try语句块和exception specifications的使用限制于非用不可的地点并且在真正异常的情况下才抛出exceptions。如果还是有性能上的问题请利用分析工具profile分析你的程序以决定“对exception的支持”是否是一个影响因素。如果是请考虑改用不同的编译器——改用一个能够以较高效率提供“C exception 处理机制”的编译器。 学习心得 本条款告诉我们异常处理机制通常会带来额外的成本只要使用try、catch、exception specification等语句则会带来5%-10%的目标代码膨胀以及对应的运行效率下降同时如果真的抛出了exception其影响将是巨大的可能导致响应时间增大3个数量级。所以非必要情况不要使用try-catch语句以及针对函数进行exception specification声明。同时如果发现exception在非用不可的情况下发现效率影响还很大考虑换个编译器试试。 事实上它不能解决至少不能广具移植性地解决。虽然许多编译器接受本页呈现的代码但标准委员会宣称“typedef内不可出现exception specification”而且没有任何解释。 ↩︎