函数try块的目的是什么?

时间:2011-12-02 16:43:17

标签: c++ exception exception-handling function-try-block

  

可能重复:
  When is a function try block useful?
  Difference between try-catch syntax for function

此类代码在构建类int内的Dog对象时抛出UseResources异常。 int异常由正常try-catch块捕获,代码输出:

Cat()  
Dog()  
~Cat()  
Inside handler

#include <iostream>
using namespace std;

class Cat
{
    public:
    Cat() { cout << "Cat()" << endl; }
    ~Cat() { cout << "~Cat()" << endl; }
};

class Dog
{
    public:
    Dog() { cout << "Dog()" << endl; throw 1; }
    ~Dog() { cout << "~Dog()" << endl; }
};

class UseResources
{
    class Cat cat;
    class Dog dog;

    public:
    UseResources() : cat(), dog() { cout << "UseResources()" << endl; }
    ~UseResources() { cout << "~UseResources()" << endl; }
};

int main()
{
    try
    {
        UseResources ur;
    }
    catch( int )
    {
        cout << "Inside handler" << endl;
    }
}

现在,如果我们使用UseResources()替换function try block构造函数的定义,如下所示,

UseResources() try : cat(), dog() { cout << "UseResources()" << endl; } catch(int) {}

输出相同

Cat()  
Dog()  
~Cat()  
Inside handler 

,即具有完全相同的最终结果。

那么,function try block的目的是什么?

2 个答案:

答案 0 :(得分:11)

想象一下,如果UseResources定义如下:

class UseResources
{
    class Cat *cat;
    class Dog dog;

    public:
    UseResources() : cat(new Cat), dog() { cout << "UseResources()" << endl; }
    ~UseResources() { delete cat; cat = NULL; cout << "~UseResources()" << endl; }
};

如果Dog::Dog()抛出,则cat会泄漏内存。因为UseResources的构造函数从未完成,该对象从未完全构建。因此它没有被称为析构函数。

要防止此泄漏,您必须使用函数级try / catch块:

UseResources() try : cat(new Cat), dog() { cout << "UseResources()" << endl; } catch(...)
{
  delete cat;
  throw;
}

为了更全面地回答你的问题,构造函数中函数级try / catch块的目的是专门进行这种清理。函数级try / catch块不能吞下异常(常规可以)。如果他们捕获了某些内容,他们会在到达catch块的末尾时再次抛出它,除非您使用throw明确地重新抛出它。您可以将一种类型的异常转换为另一种类型,但是您不能只是吞下它并继续前进,就像它没有发生一样。

这是为什么应该使用值和智能指针而不是裸指针的另一个原因,即使是类成员。因为,在您的情况下,如果您只有成员值而不是指针,则不必执行此操作。这是使用裸指针(或RAII对象中未管理的其他形式的资源)来强制这种事情。

请注意,这几乎是函数try / catch块的唯一合法用法。


更多理由不使用函数try块。上面的代码巧妙地破解了。考虑一下:

class Cat
{
  public:
  Cat() {throw "oops";}
};

那么,UseResources的构造函数会发生什么?好吧,表达式new Cat显然会抛出。但这意味着cat永远不会被初始化。这意味着delete cat将产生未定义的行为。

您可以尝试使用复杂的lambda而不仅仅是new Cat来纠正此问题:

UseResources() try
  : cat([]() -> Cat* try{ return new Cat;}catch(...) {return nullptr;} }())
  , dog()
{ cout << "UseResources()" << endl; }
catch(...)
{
  delete cat;
  throw;
}

理论上解决了这个问题,但它打破了UseResources的假定不变量。也就是说,UseResources::cat始终是有效指针。如果确实是UseResources的不变量,则此代码将失败,因为它允许构造UseResources,尽管有异常。

基本上,除非new Catnoexcept(显式或隐式),否则无法使此代码安全。

相比之下,这总是有效:

class UseResources
{
    unique_ptr<Cat> cat;
    Dog dog;

    public:
    UseResources() : cat(new Cat), dog() { cout << "UseResources()" << endl; }
    ~UseResources() { cout << "~UseResources()" << endl; }
};

简而言之,将功能级别的try-block视为严重的代码气味。

答案 1 :(得分:2)

普通函数try块的用途相对较少。它们几乎与身体内的试块相同:

int f1() try {
  // body
} catch (Exc const & e) {
  return -1;
}

int f2() {
  try {
    // body
  } catch (Exc const & e) {
    return -1;
  }
}

唯一的区别是函数try-block存在于稍大的函数范围内,而第二个构造存在于函数体范围内 - 前一个范围只能看到函数参数,后者也是本地的变量(但这不会影响两个版本的try块)。

唯一有趣的应用程序来自构造函数 -try-block:

Foo() try : a(1,2), b(), c(true) { /* ... */ } catch(...) { }

这是可以捕获其中一个初始化程序的异常的唯一方法。您不能处理异常,因为整个对象构造仍然必须失败(因此您必须以异常退出catch块,无论您是否愿意)。但是, 是从初始化列表中专门处理异常的唯一方法。

这有用吗?可能不是。 构造函数try块和以下更基本的“initialize-to-null-and-assign”模式之间基本上没有区别,这本身很糟糕:

Foo() : p1(NULL), p2(NULL), p3(NULL) {
  p1 = new Bar;
  try {
    p2 = new Zip;
    try {
      p3 = new Gulp;
    } catch (...) {
      delete p2;
      throw;
    }
  } catch(...) {
    delete p1;
    throw;
  }
}

正如你所看到的,你有一个难以维护,难以控制的混乱局面。构造函数 - 尝试块会更糟糕,因为您甚至无法分辨已经分配了多少指针。所以,如果你有两个可泄漏的分配,那么非常有用。 更新:感谢您阅读this question我因为引用成员对象是未定义的行为,实际上你根本无法使用catch块来清理资源这一事实。所以[结束更新]

简而言之:它毫无用处。