【问题标题】:Handle an exception thrown from a CTOR?处理从 CTOR 抛出的异常?
【发布时间】:2014-03-12 19:50:24
【问题描述】:

tl;博士

我想做这样的事情:

foo()
{
  Y* p;
  try {
    p = new Y();
  } catch {
    //fix any problem that may have occured
  }
  // Now I know the object was fixed during catch or simply was created successfully.
  p.do_something();
  // win
}

我正在创建的对象正在使用一种有时不可靠的永恒资源。在没有必要预防措施的情况下尝试分配是很危险的。如果它确实失败了,有些事情是可以做的。但是,与我稍后展示的 switch 语句不同,似乎抛出异常不允许我解决 AFAIK 问题。

有人建议我从构造函数中抛出异常。但是,我不明白这如何证明是有用的,因为我看不到该异常将如何处理?我进行了搜索并找到了this example,但我不确定它是否真的是一种有用的可扩展方式来处理构造失败。

示例代码:

void f()
{
  X x;             //← if X::X() throws, the memory for x itself will not leak
  Y* p = new Y();  //← if Y::Y() throws, the memory for *p itself will not leak
}

假设在p 之前分配的堆上的实例更多。他们不会因此而泄漏到内存中吗?所以函数f只是用来构造Y的一个实例。将“危险”构造移到另一种方法之外不是总是更有用吗?

我通常会做什么:

X* x = new X(); //No dangerous operation

switch (x.init()) // init returns int
  {
    case ...
    // Handle failed init() here
  }

这样做有缺点吗?好像比较靠谱。

【问题讨论】:

  • 不要使用new。然后问题就消失了。
  • 或者,如果您必须使用new,请使用unique_ptrshared_ptr,这样当您离开作用域时,内存就会被释放。
  • 更好的是对所有资源进行 raii-wrap,而不仅仅是内存。
  • 也许你的构造函数有问题。如果你通过抛出异常来解决这个问题,那么你有两个问题。
  • 问题与new无关。即使你在堆栈上构造一个对象或将一个对象插入到容器中,构造函数仍然会执行。

标签: c++ exception constructor


【解决方案1】:

您应该在每个成员上使用RAII(资源获取是初始化)。仅当构造函数完成时才调用对象的析构函数。这样,如果只调用两个成员的构造函数中的一个,则只会调用该成员的析构函数,并在其中执行任何清理操作。

class Locked_file_handle {
    File_ptr p;
    unique_lock<mutex> lck;
public:
    X(const char* file, mutex& m)
        :p{file, "rw"},
        lck{m} 
    {}
    // ..
};

如果在 p 构造之后但在 lck 之前发生异常,则将调用 p 而不是 lck 的析构函数。这样做可以让构造函数的作者放弃编写显式的异常处理代码。

来源:“C++ 编程语言第 4 版”(Stroustrup)

编辑:所以在你的情况下,它看起来像这样

public class YHandle {
    Y* p;
    YHandle() {
        Y = new Y()
    }
    ~YHandle() {
        delete Y;
    }
}

foo() {
    YHandle p = YHandle();
    p.do_something();
} // p is deleted here

【讨论】:

  • 您能否评论一下此代码示例中与 RAII 相关的内容,以及它如何使可能对此感到困惑的人受益? AFAIK RAII 表示需要在 X 的 DTOR 期间释放锁?
  • 上述构造函数中唯一可能发生异常的地方是这两个本地对象的构造函数之一 - p 和 lck。我们假设这两个对象的析构函数都正确地释放了任何资源(这将因资源而异),或者随后也在其成员上使用 RAII。
【解决方案2】:

您的示例是安全的,因为如果 c'tor 抛出异常,则为构造失败的对象分配的内存在传递异​​常之前被释放。当异常退出f 范围时,堆栈被展开。

如果将一个半构造并随后具有其成员和基数的对象返回到调用范围,则该范围将无法确定如何正确释放该内存,因此幸好这还没有完成。

如果在同一个函数中在堆上构造其他对象,则应用通常的规则。原则上,这里不需要特殊情况的 c'tor 调用,只需假设抛出的语句是常规函数调用即可。

【讨论】:

  • 但是,在构造函数内部,设计者仍应尝试确保资源被释放。如果构造函数分配了 5 个东西,然后在做第 6 个东西的时候扔了怎么办?除非构造函数中有 try..catch 在重新抛出之前进行一些清理,否则可能会发生内存泄漏。
  • 当然。在这方面,c'tor 与任何其他函数没有什么不同。
【解决方案3】:

如果抛出可能导致泄漏,那么删除分配是你的责任

您可以像这样滚动异常:

void f()
{
  X x;             //← if X::X() throws, the memory for x itself will not leak
  Y* p;
  try{  
    p = new Y();  //← if Y::Y() throws, the memory for *p itself will not leak
  } catch(exception &e){
    ///clean what you need

    throw e;
  }
}

【讨论】:

  • 我现在明白了。但是,如果无法在函数中解决潜在问题,那么抛出异常有什么意义呢?这不就像用另一个抽象来包装构造函数,把繁重的工作留给其他人吗?
  • 因为构造函数不能返回值,这是报告失败的最合乎逻辑的方式。
  • 调用一个不做任何“危险”事情的构造函数然后调用一个返回信息(如果失败)的公共 init() 方法不是更合乎逻辑吗?我会把这个添加到问题中
  • 使用init() 函数是不受欢迎的,即使是语言作者 Stroustrup 本人也是如此。当构造函数完成执行时,您应该留下一个完全可用的对象。
  • 使用初始化函数没有任何好处。无论哪种方式,您仍然可能需要处理异常。如果 init 分配内存,然后抛出,那么你会做什么与构造函数抛出时你会做的有什么不同?创建一个 init 函数只会给您一种错误的感觉,即一切都很好,但无论哪种方式,您仍然需要进行完全相同的清理工作。使用 init 函数,您还有更多工作要做,因为现在您必须销毁 *p。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-10
  • 2011-11-23
  • 1970-01-01
相关资源
最近更新 更多