C与C++中的异常处理14 (转)

C与C++中的异常处理14 (转)[@more@]

1.  模板安全XML:namespace prefix = o ns = "urn:schemas-microsoft-com:Office:office" />

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

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

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

template

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 i;

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

template <>

class wrapper

  {

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

 

1.1  class类型的参数

  现在看:

wrapper x;

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

template <>

class wrapper

  {

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的默认构造函数。这个构造函数可能抛异常。

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

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

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

  由于这样的不确定性,我们需要采用保守的策略:假设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块的局限性,并承诺要探究其原因的。我所联系的业内专家没人知道确切答案。现在唯一的共识是:

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

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

  我写信给了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

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()上的异常安全问题,和今天的内容同样精彩。


来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10752043/viewspace-992220/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/10752043/viewspace-992220/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值