13.5 延迟计算
为一个最终可能不需要的计算付出性能代价显然不是明智之举。然而在复杂的代码中这种情况比比皆是。我们不应该执行"只在某种情况下"才需要的昂贵计算,而应该只在必要的时候执行昂贵计算。这通常常意味着把计算推迟到真正需要的时候才进行,因此称之为延迟计算(Lazy eva luation)[Mey96, ES90]。
早在纯C程序时代,我们就习惯在程序的开头预先定义所有的变量。而在C++(www.cppentry.com)中,对象的定义会调用构造函数和析构函数,这可能是高成本的,因为它导致了立即计算--而这正是我们所要避免的。延迟计算原则建议我们推迟对象的定义,直到要使用该对象时再定义。为不一定用到的构造函数和析构函数付出代价是没有意义的。这听起来很可笑,实际上却时常发生。
在一个用C++(www.cppentry.com)编写的网关程序中,我们有一段运行在AIX内核的对程序性能至关重要的代码。这段代码用于交换下行和上行通信适配器的消息。在程序定义的对象中,有一个对象的构造和析构函数代价很昂贵:
- int route(Message *msg)
- {
- ExpensiveClass upstream(msg);
-
- if (goingUpstream) {
- ... // 处理代价昂贵的对象
- }
-
- //此处不使用对象upstream
-
- return SUCCESS;
- }
使用upstream对象的代价很高。原先的代码急于在函数开始时构造该对象,而事实上在程序中只有一半的时间,即消息被交换至上行时才使用它,在另一半时间,即消息被交换至下行时根本不使用。对于后一种情况,upstream对象的计算完全是浪费的。更好的办法是在真正需要upstream对象的地方定义它:
- int route(Message *msg)
- {
-
- if (goingUpstream) {
- ExpensiveClass upstream(msg);
-
- //处理代价昂贵对象
- }
-
- //此处不使用对象upstream
-
- return SUCCESS;
- }
在[Mey97]的Item 32中,Meyers阐述了另一条关于延迟计算的重要结论:不仅应该将对象的创建推迟至合适的位置,而且应该直到具备了一个有效创建操作所必需的全部条件后,再创建对象。例如:
- void f()
- {
- string s; // 1
- ...
- char *p = "Message"; // 2
- ...
- s = p; // 3
- ...
- }
在语句1中,我们调用默认构造函数String:: String()创建了String对象s,之后在语句3中我们赋予s真实内容。直到执行完语句3,对象s才算真正创建完成,因此这是一种低效的构造。它需要以下操作:
默认的string构造函数--在语句1中。
赋值运算符--在语句3中。
延迟计算原则可以将上述操作缩减为仅调用一次构造函数。我们将String型对象s的构造推迟到已具备必需条件后实现。
- void f()
- {
- char *p = "Message";
- string s(p); //调用String::String(char*)
- ...
- }
通过推迟构造s,我们实现了一步构造--这无疑更为高效。