C与C++中的异常处理14

类别:VC语言 点击:0 评论:0 推荐:
1.     模板安全

    上次,我开始讨论异常安全。这次,我将探究模板安全。

    模板根据参数的类型进行实例化。因为通常事先不知道其具体类型,所以也无法确切知道将在哪儿产生异常。你大概最期望的就是去发现可能在哪儿抛异常。这样的行为很具挑战性。

    看一下这个简单的模板类:

template <typename T>

class wrapper

   {

public:

   wrapper()

      {

      }

   T get()

      {

      return value_;

      }

   void set(T const &value)

      {

      value_ = value;

      }

private:

   T value_;

   wrapper(wrapper const &);

   wrapper &operator=(wrapper const &);

   };

 

    如名所示,wrapper包容了一个T类型的对象。方法get()和set()得到和改变私有的包容对象value_。两个常用方法--拷贝构造函数和赋值运算符没有使用,所以没有定义,而第三个--析构函数由编译器隐含定义。

    实例化的过程很简单,例如:

wrapper<int> i;

包容了一个int。i的定义过程导致编译器从模板实例化了一个定义为wrapper<int>的类:

template <>

class wrapper<int>

   {

public:

   wrapper()

      {

      }

   int get()

      {

      return value_;

      }

   void set(int const &value)

      {

      value_ = value;

      }

private:

   int value_;

   wrapper(wrapper const &);

   wrapper &operator=(wrapper const &);

   };

 

    因为wrapper<int>只接受int或其引用(一个内嵌类型或内嵌类型的引用),所以不会触及异常。wrapper<int>不抛异常,也没有直接或间接调用任何可能抛异常的函数。我不进行正规的分析了,但相信我:wrapper<int>是异常安全的。

 

1.1     class类型的参数

    现在看:

wrapper<X> x;

这里X是一个类。在这个定义里,编译器实例化了类wrapper<X>:

template <>

class wrapper<X>

   {

public:

   wrapper()

      {

      }

   X get()

      {

      return value_;

      }

   void set(X const &value)

      {

      value_ = value;

      }

private:

   X value_;

   wrapper(wrapper const &);

   wrapper &operator=(wrapper const &);

   };

 

    粗一看,这个定义没什么问题,没有触及异常。但思考一下:

l         wrapper<X>包容了一个X的子对象。这个子对象需要构造,意味着调用了X的默认构造函数。这个构造函数可能抛异常。

l         wrapper<X>::get()产生并返回了一个X的临时对象。为了构造这个临时对象,get()调用了X的拷贝构造函数。这个构造函数可能抛异常。

l         wrapper<X>::set()执行了表达式value_ = value,它实际上调用了X的赋值运算。这个运算可能抛异常。

    在wrapper<int>中针对不抛异常的内嵌类型的操作现在在wrapper<X>中变成调用可能抛异常的函数了,同样的模板,同样的语句,但极其不同的含义。

    由于这样的不确定性,我们需要采用保守的策略:假设wrapper会根据类来实例化,而这些类在其成员上没有异常规格申明,它们可能抛异常。

 

1.2     使得包容安全

    再假设wrapper的异常规格申明承诺其成员不产生异常。至少,我们必须在其成员上加上异常规格申明throw()。我们需要修补掉这些可能导致异常的地方:

l         在wrapper::wrapper()中构造value_的过程。

l         在wrapper::get()中返回value_的过程。

l         在wrapper::set()中对value_赋值的过程。

    另外,在违背throw()的异常规格申明时,我们还要处理std::unexpected。

1.3     Leak #1:默认构造函数

    对wrapper的默认构造函数,解决方法看起来是采用function try块:

wrapper() throw()

   try : T()

      {

      }

   catch (...)

      {

      }

    虽然很吸引人,但它不能工作。根据C++标准(paragraph 15.3/16,“Handling an exception”):

    对构造或析构函数上的function-try-block,当控制权到达了异常处理函数的结束点时,被捕获的异常被再次抛出。对于一般的函数,此时是函数返回,等同于没有返回值的return语句,对于定义了返回类型的函数此时的行为为未定义。

    换句话说,上面的程序相当于是:

X::X() throw()

   try : T()

      {

      }

   catch (...)

      {

      throw;

      }

    这不是我们想要的。

    我想过这样做:

X::X() throw()

   try

      {

      }

   catch (...)

      {

      return;

      }

但它违背了标准的paragraph 15:

    如果在构造函数上的function-try-block的异常处理函数体中出现了return语句,程序是病态的。

    我被标准卡死了,在用支持function try块的编译器试验后,我没有找到让它们以我所期望的方式运行的方法。不管我怎么尝试,所有被捕获的异常都仍然被再次抛出,违背了throw()的异常规格申明,并打败了我实现接口安全的目标。

 

原则:无法用function try块来实现构造函数的接口安全。

引申原则1:尽可能使用构造函数不抛异常的基类或成员子对象。

引申原则2:为了帮助别人实现引申原则1,不要从你的构造函数中抛出任何异常。(这和我在Part13中所提的看法是矛盾的。)

我发现C++标准的规则非常奇怪,因为它们减弱了function try的实际价值:在进入包容对象的构造函数(wrapper::wrapper())前捕获从子对象(T::T())构造函数中抛出的异常。实际上,function try块是你捕获这样的异常的唯一方法;但是你只能捕获它们却不能处理掉它们!

 

(WQ注:下面的文字原载于Part15上,我把提前了。

上次我讨论了function try块的局限性,并承诺要探究其原因的。我所联系的业内专家没人知道确切答案。现在唯一的共识是:

l         如我所猜测,标准委员会将function try块设计为过滤而不是捕获子对象构造函数中发生的异常的。

l         可能的动机是:确保没人误用没有构造成功的包容对象。

    我写信给了Herb Sutter,《teh Exceptional C++》的作者。他从没碰过这个问题,但很感兴趣,以至于将其写入“Guru of the Week”专栏。如果你想加入这个讨论,到新闻组comp.lang.c++.moderated上去看“Guru of the Week #66: Constructor Failures”。

 

    注意function try可以映射或转换异常:

X::X()

   try

      {

      throw 1;

      }

   catch (int)

      {

      throw 1L; // map int exception to long exception

      }

 

    这样看,它们非常象unexpected异常的处理函数。事实上,我现在怀疑这才是它们的设计目的(至少是对构造函数而言):更象是个异常过滤器而不是异常处理函数。我将继续研究下去,以发现这些规则后面的原理。

    现在,至少,我们被迫使用一个不怎么直接的解决方法:

template <typename T>

class wrapper

   {

public:

   wrapper() throw()

         : value_(NULL)

      {

      try

         {

         value_ = new T;

         }

      catch (...)

         {

         }

      }

   // ...

private:

   T *value_;

   // ...

   };

 

    被包容的对象,原来是在wrapper::wrapper()进入前构造的,现在是在其函数体内构造的了。这个变化可以让我们使用普通的方法来捕获异常而不用function try块了。

    因为value_现在是个T *而不是T对象了,get()和set()必须使用指针的语法了:

T get()

   {

   return *value_;

   }

 

void set(T const &value)

   {

   *value_ = value;

   }

 

1.4     Leak #1A:operator new

    在构造函数内的try块中,语句

value_ = new T;

隐含地调用了operator new来分配*value_的内存。而这个operator new函数可能抛异常。

    幸好,我们的wrapper::wrapper()能同时捕获T的构造函数和operator new函数抛出的异常,因此维持了接口安全。但,记住这个关键性的差异:

l         如果T的构造函数抛了异常,operator delete被隐含调用了来释放分配的内存。(对于placement new,这取决于是否存在匹配的operator delete,我在part 8和9说过了的。)

l         如果operator new抛了异常,operator delete不会被隐含调用。

    第二点本不该有什么问题:如果operator new抛了异常,通常是因为内存分配失败,operator delete没什么需要它去释放的。但,如果operator new成功分配了内存但因为其它原因而仍然抛了异常,它必须负责释放内存。换句话说,operator new自己必须是行为安全的。

    (同样的问题也发生在通过operator nwe[]创建数组时。)

1.5     Leak #1B:Destructor

    想要wrapper行为安全,我们需要它的析构函数释放new出来的内存:

~wrapper() throw()

   {

   delete value_;

   }

    这看起来很简单,但请等一下说大话!delete value_调用*value_的析构函数,而这个析构函数可能抛异常。要实现~wrapper()的接口异常,我们必须加上try块:

~wrapper() throw()

   {

   try

      {

      delete value_;

      }

   catch (...)

      {

      }

   }

 

    但这还不够。如果*value_的析构函数抛了异常,operator delete不会被调用了来释放*value_的内存。我们需要加上行为安全:

~wrapper() throw()

   {

   try

      {

      delete value_;

      }

   catch (...)

      {

      operator delete(value_);

      }

   }

 

    仍然没结束。C++标准运行库申明的operator delete为

void operator delete(void *) throw();

它是不抛异常了,但自定义的operator delete可没说不抛。要想超级安全,我们应该写:

~wrapper() throw()

   {

   try

      {

      delete value_;

      }

   catch (...)

      {

      try

         {

         operator delete(value_);

         }

      catch (...)

         {

         }

      }

   }

 

    但这还存在危险。语句

delete value_;

隐含调用了operator delete。如果它抛了异常,我们将进入catch块,一步步执行下去并再次调用同样的operator delete!我们将程序连续暴露在同样的异常下。这不会是个好程序的。

    最后,记住:operator delete在被new出对象的构造函数抛异常时被隐含调用。如果这个被隐含调用的operator delete也抛了异常,程序将处于两次异常状态并调用terminate()。

原则:不要在一个可能在异常正被处理过程被调用的函数中抛异常。尤其是,不要从下列情况下抛异常:

l         destructors

l         operator delete

l         operator delete[]

    几个小习题:用auto_ptr代替value_,然后重写wrapper的构造函数,并决定其虚构函数的角色(如果需要的话),条件是必须保持异常安全。

1.6     题外话

    我本准备一次维持异常安全的。但现在是第二部分,并仍然有足够的素材写成第三部分(我发誓那是最后的部分)。下次,我将讨论get()和set()上的异常安全问题,和今天的内容同样精彩。

本文地址:http://com.8s8s.com/it/it2712.htm