在构造函数中抛出异常是不安全的?

时间:2009-07-29 00:58:38

标签: c++

我知道从析构函数中抛出异常是不安全的,但是从构造函数中抛出异常是不安全的?

e.g。对于全局声明的对象会发生什么?用gcc进行快速测试,我得到了 一个中止,总能得到保证吗?您将使用什么解决方案来满足这种情况?

是否有任何情况下构造函数可以抛出异常并且不会留下我们期望的结果。

编辑:我想我应该补充一点,我试图了解在什么情况下我可以获得资源泄漏。看起来明智的做法是手动释放我们在抛出异常之前通过构造获得的资源。我从来不需要在今天之前在构造函数中抛出异常,所以试图理解是否存在任何陷阱。

即。这也安全吗?

class P{
  public:
    P() { 
       // do stuff...

       if (error)
          throw exception
    }
}

dostuff(P *p){
 // do something with P
}

... 
try {
  dostuff(new P())
} catch(exception) {

}

是否会释放分配给对象P的内存?

EDIT2:忘记提及在这种特殊情况下,dostuff将对P的引用存储在输出队列中。 P实际上是一条消息,dostuff接收消息,将其路由到适当的输出队列并发送它。基本上,一旦dostuff持有它,它会在后来的dostuff内部释放。我想我想把一个autoptr放在P周围并在dostuff后调用autoptr上的释放以防止内存泄漏,这是正确的吗?

5 个答案:

答案 0 :(得分:29)

从构造函数中抛出异常是good thing。当构造函数中的某些内容失败时,您有两个选择:

  • 保持“僵尸”状态,该类存在但不执行任何操作,或
  • 抛出异常。

维护僵尸课程可能会非常麻烦,当真正的答案应该是,“这失败了,现在是什么?”。

根据3.6.2.4的标准:

  

如果非本地静态对象的构造或破坏以抛出未捕获的异常而结束,则结果是调用terminate(18.6.3.3)。

终止是指std::terminate


关于你的例子,没有。这是因为您没有使用RAII concepts。抛出异常时,堆栈将被展开,这意味着当代码到达最接近的相应catch子句时,所有对象都会调用它们的析构函数。

指针没有析构函数。让我们做一个简单的测试用例:

#include <string>

int main(void)
{
    try
    {
        std::string str = "Blah.";
        int *pi = new int;

        throw;

        delete pi; // cannot be reached
    }
    catch(...)
    {
    }
}

在这里,str将分配内存,并复制“Blah”。进入它,pi将被初始化为指向内存中的整数。

抛出异常时,堆栈展开开始。它将首先“调用”指针的析构函数(什么也不做),然后是str的析构函数,它将释放分配给它的内存。

如果您使用RAII概念,则使用智能指针:

#include <memory>
#include <string>

int main(void)
{
    try
    {
        std::string s = "Blah.";
        std::auto_ptr<int> pi(new int);

        throw;

        // no need to manually delete.
    }
    catch(...)
    {
    }
}

在这里,pi的析构函数将调用delete并且不会泄漏任何内存。这就是为什么你应该总是包装你的指针,这与我们使用std::vector而不是手动分配,调整大小和释放指针的原因相同。 (清洁和安全)

修改

我忘了提。你问这个:

  

我想我想把一个autoptr放在P周围并在dostuff之后调用autoptr上的释放以防止内存泄漏,这是正确的吗?

我没有明确说明,只是在上面暗示,但答案是没有。您所要做的就是将其放在auto_ptr内,到时候,它会自动删除。手动释放它会使首先将其放入容器中的目的失败。

我还建议您查看更高级的智能指针,例如boost中的指针。一个非常受欢迎的版本是shared_ptrreference counted,使其适合存储在容器中并被复制。 (与auto_ptr不同。在容器中使用auto_ptr!)

答案 1 :(得分:8)

作为Spence mentioned,如果没有仔细编写构造函数来处理这种情况,那么从构造函数抛出(或允许异常来转义构造函数)可能会泄漏资源。

这是使用RAII对象(如智能指针)应该受到青睐的一个重要原因 - 它们会在异常情况下自动处理清理。

如果您的资源需要删除或以其他方式手动释放,您需要确保在异常离开之前清理它们。这听起来并不总是那么容易(当然也不像让RAII对象自动处理它那么容易)。

不要忘记,如果你需要手动处理构造函数初始化列表中发生的事情,你需要使用时髦的'function-try-block'语法:

C::C(int ii, double id)
try
     : i(f(ii)), d(id)
{
     //constructor function body
}
catch (...)
{
     //handles exceptions thrown from the ctor-initializer
     //and from the constructor function body
}

另外,请记住,异常安全是“交换”习惯获得广泛青睐的主要原因(唯一的原因) - 这是确保复制构造函数在异常情况下不泄漏或损坏对象的简单方法。

所以,底线是使用异常来处理构造函数中的错误很好,但它不一定是自动的。

答案 2 :(得分:3)

当您从构造函数中抛出异常时,会发生一些事情。

1)所有完全构造的成员都将被称为destructors 2)将释放为该对象分配的内存。

为了帮助自动化你不应该在你的类中使用RAW指针,标准的智能指针之一通常会做到这一点,编译器优化会将大部分开销减少到几乎没有[或只有你应该有的工作无论如何都在手动做。]

将指针传递给函数

我不会做的另一件事;将值作为指针传递给函数 这里的问题是你没有说明谁拥有该对象。如果没有隐含的所有权信息,就不清楚(像所有C函数一样)谁可以清理指针。

dostuff(P *p)
{
    // do something with P
}

你提到p存储在一个que中并在稍后使用。这意味着您将对象的所有权传递给函数。因此,使用std :: auto_ptr使这个关系显式化。通过这样做,dostuff()的调用者知道在调用dostuff()之后他不能使用指针,因为调用该函数的行为实际上已经将指针转移到函数中(即调用者本地auto_ptr将包含一个NULL指针对dostuff()的调用。

void doStuff(std::auto_ptr<P> p)
{
    // do something with p
    //
    // Even if you do nothing with the RAW pointer you have taken the
    // pointer from the caller. If you do not use p then it will be auto
    // deleted.
}


int main()
{
    // This works fine.
    dostuff(std::auto_ptr<P>(new P));


    // This works just as well.
    std::auto_ptr<P>    value(new P);
    dostuff(value);

    // Here you are guranteed that value has a NULL pointer.
    // Because dostuff() took ownership (and physically took)
    // of the pointer so it could manage the lifespan.
}

在投币器中存储指针

您提到dostuff()用于存储p对象列表以进行延迟处理 所以这意味着您将对象粘贴到容器中。现在普通的容器不支持std :: auto_ptr。但是boost确实支持指针容器(容器取得所有权)。此外,这些容器了解auto_ptr并自动将所有权从auto_ptr转移到容器。

boost::ptr_list<P>   messages;
void doStuff(std::auto_ptr<P> p)
{
    messages.push_front(p);
}

注意当您访问这些容器的成员时,它总是返回包含对象的引用(而不是指针)。这表明对象的生命周期与容器的生命周期有关,只要容器有效,引用就是有效的(除非您明确删除了对象)。

答案 3 :(得分:3)

以前的答案非常好。我只想根据Martin York和Michael Burr的答案添加一件事。

使用Michael Burr的示例构造函数,我在构造函数体中添加了一个赋值:

C::C(int ii, double id)
try
     : i(f(ii)), d(id)
{
     //constructor function body

     d = sqrt(d);

}
catch (...)
{
     //handles exceptions thrown from the ctor-initializer
     //and from the constructor function body
}

现在的问题是,d被认为是'完全构造'时,如果在构造函数中抛出异常(如在Martin的帖子中),将调用它的析构函数?答案是:在初始化程序之后

 : i(f(ii)), d(id)

关键是,如果从构造函数 body 抛出异常,则对象的字段将始终调用其析构函数。 (无论你是否为它们实际指定了初始值设定项,都是如此。)相反,如果从另一个字段的初始化程序抛出异常,则对于初始化程序已经运行的那些字段,将调用析构函数 (这些字段的。)

这意味着最佳做法是不允许任何字段以不可构造的值(例如,未定义的指针)到达构造函数体。在这种情况下,最好通过初始化器为您的字段提供其实际值,而不是(比如说)首先将指针设置为NULL,然后在构造函数体中为它们提供“真实”值。

答案 4 :(得分:0)

如果您在构造函数中使用资源(例如套接字等),那么如果您抛出异常,这将会泄露吗?

但我想这是在构造函数中没有工作的论据,懒得在你需要时初始化你的连接。