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

原创 2002年03月01日 08:51:00

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

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

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

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

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;

包容了一个inti的定义过程导致编译器从模板实例化了一个定义为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 #1Aoperator new

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

value_ = new T;

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

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

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

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

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

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

1.5     Leak #1BDestructor

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

C/C++异常处理的对比

本文主要介绍C异常处理与C++异常处理的区别。 包括errno、signal、nonlocal goto、异常的捕获、异常规格说明(exception specification)、标准异常对象等。...
  • yeming81
  • yeming81
  • 2010年06月16日 00:19
  • 4699

JAVA异常处理 与C++的不同

*Java异常处理模型   对于一个非常熟悉 C++ 异常处理模型的程序员来说,它几乎可以不经任何其它培训和学习,就可以完全接受和能够轻松地使用 Java 语言中的异常处理编程方法。这是因为 Java...
  • ljlove2008
  • ljlove2008
  • 2008年10月14日 23:32
  • 2472

Visual C++异常处理机制原理与应用(三)——C/C++结构化异常处理之try-except异常处理的使用(上)

在微软的VC++中,C/C++结构化异常处理机制一共包含两部分内容:终止处理程序和异常处理程序,本文主要介绍异常处理程序的相关内容。...
  • LPWSTR
  • LPWSTR
  • 2017年12月03日 21:45
  • 109

MATLAB与c/c++之矩阵操作差别

1)MATLAB默认数组(矩阵)访问下标是从1开始的,而c/c++默认是从0开始; 2)MATLAB的二位数组(矩阵)的数据存放顺序默认为列优先(从第一列自上向下存放和访问,再第二列。。。。),而...
  • wonengguwozai
  • wonengguwozai
  • 2016年10月10日 21:23
  • 627

C++异常处理机制总结

参考文档:《C++编程思想》《C++Primer》《More effective C++》 一、             传统的错误处理机制: 1.         返回值或全局错误状态标志。缺点:需...
  • MulinB
  • MulinB
  • 2007年08月29日 10:07
  • 2268

Unix/Linux C++应用开发-异常以及错误处理

计算机应用程序中离不开错误处理,尤其是生产型大型软件系统。应用软件系统运行属于循环处理事务,出错后需要保证不能让软件程序直接退出。这就需要使用一定的程序容错处理来应对。一般情况下,大型软件开发中的软件...
  • wangfengwf
  • wangfengwf
  • 2013年09月11日 21:15
  • 30140

C++的和Java的异常机制

    程序总会出现异常的,需要我们去处理。C++和JAVA都有自己异常机制,我们应该遵循着去处理异常。那它们的异常机制有何异同呢?    要注意一点:异常机制处理异常是要付出代价的,即异常处理的代码...
  • Windy83
  • Windy83
  • 2006年11月17日 01:37
  • 1929

c++primer之try语句块和异常处理

try语句块和异常处理。。异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。处理反常行为可能是设计所有系统最难的一部分。 。。当我们的...
  • u014365862
  • u014365862
  • 2015年08月16日 22:03
  • 904

C++异常处理实例

/************************************************************************************************ *...
  • JarvisChu
  • JarvisChu
  • 2011年07月22日 22:41
  • 4027

Jni C/C++运行时遇到异常怎么办?捕获与抛出

有个头疼的问题,Jni C/C++遇到问题闪退怎么办?有办法,我们可以在异常发生后通过判断清除异常解决,保持程序及时反应处理。 比如: package crash; import java.se...
  • zhangbuzhangbu
  • zhangbuzhangbu
  • 2016年10月26日 23:09
  • 2147
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C与C++中的异常处理14
举报原因:
原因补充:

(最多只允许输入30个字)