天天看点

[C++]异常处理中的拷贝构造操作(copy constructor)

*书生注:就算More Effective C++的读后感吧

[问题]

下面这段代码中,类型T的复制拷贝操作(copy constructor)一共被调用几次?

如何改进来减少调用次数?能减少到几次?

class T {
public:
   T(constT& t) {}
    T() {}
};

void f() throw(T) {
    T t;
    throw t;
}

int main() {
    try {
       f();
    } catch (Tt) {
       throw t;
    }
}
           

[解答]

答案是:3次。

那么能减少到几次呢?恩,1次。某些编译器上甚至可以到0次。

让我们一个一个来看。

明白了原因,也就知道如何来改进了。

class T {
public:
    T(constT& t) {}
    T(){}
};

void f() throw(T) {
   T t;
   throw t;          //第一次!
}

int main(){   
    try {
      f();
   } catch (T t){   //第二次!
       throw t;      //第三次!
   }
}
           

让我们按照由易到难的顺序,也就是,倒过来说。

[第三次]

当throw一个明确对象的时候,一定会发生复制。

道理很简单,如果把当前的auto变量(在我们这里是t)直接throw出去的话,

在紧接着离开函数f的作用域时,auto变量会被退栈过程销毁(析构),

那么外层的代码得到的异常就可能是一个已经仙去的对象(oh!)。

就我们这里的代码而言,针对对象t,会做一个拷贝构造操作(copy constructor),

产生一个新的对象出来,用来抛出去。

就这里的代码而言,有什么办法来改进吗?

有,那就是不带任何对象的throw语句,重新抛出当前的异常,避免发生复制操作。

[第二次]

当catch一个异常的时候,如果采用by value的方式,一定会发生复制。

原理和函数传参方式类似。

同样的道理,我们可以采用by reference方式来避免多余的复制操作。

(就异常而言,复制操作完全是多余的。而且为reference加上const修饰符也是多余的。

  因为接收到的异常本身就已经是一个复制后的对象。)

[第一次]

这里的复制和第三次类似,但是这里我们必须抛出一个明确的异常,

不能像第三次的解决办法那样重新抛出当前的异常(因为还没有异常发生!),

似乎没有什么可改进的了,是吗?

不是的,c++允许编译器针对临时变量(temp variable)进行优化,

所以这里我们可以通过一个匿名临时变量来处理。

throw T();

编译器就有机会可以优化掉中间产生的临时变量,直接将default construtor创建的对象抛出去。

(不保证所有编译器都会做此优化,但,匿名临时变量至少写法上更简洁,不是吗?)

下面是修改后的代码(在g++ 4.1.2上验证通过)

class T {
public:  
    T(constT& t) {}
    T(){}
};

void f() throw(T) {
   throw T();        //没有复制!(某些编译器下)
}

int main(){   
    try {
      f();
   } catch (T& t){   //没有复制!
       throw;       //没有复制!
   }
}
           

[总结]

(1)采用by reference方式来catch异常。

(2)通过throw来重新抛出异常。

(3)如果可以的话,尽可能采用匿名临时变量的写法。

继续阅读