从构造函数中抛出异常

lkr*_*sen 265 c++ constructor exception throw

我正在和一位同事讨论如何从构造函数中抛出异常,并且我想要一些反馈.

从设计的角度来看,从构造函数中抛出异常是否可以?

假设我在一个类中包装一个POSIX互斥锁,它看起来像这样:

class Mutex {
public:
  Mutex() {
    if (pthread_mutex_init(&mutex_, 0) != 0) {
      throw MutexInitException();
    }
  }

  ~Mutex() {
    pthread_mutex_destroy(&mutex_);
  }

  void lock() {
    if (pthread_mutex_lock(&mutex_) != 0) {
      throw MutexLockException();
    }
  }

  void unlock() {
    if (pthread_mutex_unlock(&mutex_) != 0) {
      throw MutexUnlockException();
    }
  }

private:
  pthread_mutex_t mutex_;
};
Run Code Online (Sandbox Code Playgroud)

我的问题是,这是标准的方法吗?因为如果pthread mutex_init调用失败,则互斥对象不可用,因此抛出异常可确保不会创建互斥锁.

我是否应该为Mutex类创建一个成员函数init,并pthread mutex_init在其中调用将返回基于返回的bool pthread mutex_init?这样我就不必为这种低级对象使用异常.

Nav*_*een 250

是的,从失败的构造函数中抛出异常是执行此操作的标准方法.阅读此常见问题解答有关处理失败的构造函数以获取更多信息.使用init()方法也可以,但创建互斥对象的每个人都必须记住必须调用init().我认为这违反了RAII原则.

  • 在大多数情况下.别忘了像std :: fstream这样的东西.失败时它仍会创建一个对象,但因为我们总是测试对象的状态,所以它运行良好.因此,具有在正常使用情况下测试的自然状态的对象可能不需要抛出. (13认同)
  • 不是真的,在这种特殊情况下,请注意他的Mutex析构函数永远不会被调用,可能会泄漏pthread互斥体.解决方法是使用智能指针作为pthread互斥锁,更好地使用boost互斥锁或std :: mutex,当有更好的替代方案时,没有理由继续使用旧的功能样式OS结构. (4认同)
  • @Martin York:我不确定 std::fstream 是一个很好的例子。是的。它确实依赖于构造函数后的错误检查。但应该吗?这是一个糟糕的设计,可以追溯到 C++ 版本,其中禁止构造函数抛出异常。 (2认同)

Fer*_*cio 97

如果从构造函数中抛出异常,请记住,如果需要在构造函数初始化列表中捕获该异常,则需要使用函数try/catch语法.

例如

func::func() : foo()
{
    try {...}
    catch (...) // will NOT catch exceptions thrown from foo constructor
    { ... }
}
Run Code Online (Sandbox Code Playgroud)

func::func()
    try : foo() {...}
    catch (...) // will catch exceptions thrown from foo constructor
    { ... }
Run Code Online (Sandbox Code Playgroud)

  • 应该注意的是,无法抑制从子对象构造中引发的异常:http://www.gotw.ca/gotw/066.htm (34认同)

小智 34

抛出异常是处理构造函数失败的最佳方法.您应该特别避免半构造一个对象,然后依靠类的用户通过测试某种类型的标志变量来检测构造失败.

在一个相关的点上,你有几种不同的异常类型来处理互斥错误的事实让我有点担忧.继承是一个很好的工具,但它可以被过度使用.在这种情况下,我可能更喜欢单个MutexError异常,可能包含信息性错误消息.


小智 15

#include <iostream>

class bar
{
public:
  bar()
  {
    std::cout << "bar() called" << std::endl;
  }

  ~bar()
  {
    std::cout << "~bar() called" << std::endl;

  }
};
class foo
{
public:
  foo()
    : b(new bar())
  {
    std::cout << "foo() called" << std::endl;
    throw "throw something";
  }

  ~foo()
  {
    delete b;
    std::cout << "~foo() called" << std::endl;
  }

private:
  bar *b;
};


int main(void)
{
  try {
    std::cout << "heap: new foo" << std::endl;
    foo *f = new foo();
  } catch (const char *e) {
    std::cout << "heap exception: " << e << std::endl;
  }

  try {
    std::cout << "stack: foo" << std::endl;
    foo f;
  } catch (const char *e) {
    std::cout << "stack exception: " << e << std::endl;
  }

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出:

heap: new foo
bar() called
foo() called
heap exception: throw something
stack: foo
bar() called
foo() called
stack exception: throw something
Run Code Online (Sandbox Code Playgroud)

析构函数不会被调用,所以如果需要在构造函数中抛出异常,那么很多东西(例如清理?)要做.

  • 您应该使用std :: unique_ptr或类似的.如果在构造期间抛出异常,则调用成员的析构函数,但是普通指针没有任何异常.用'std :: unique_ptr <bar> b`替换`bar*b`(你必须删除`delete b;`并添加`<memory>`标题),然后重新运行. (10认同)
  • 很好的观点。我很惊讶没有其他答案可以解决这种类型的泄漏。 (2认同)
  • 这种行为非常明智.如果构造函数失败(未成功完成),为什么要调用析构函数?它没有什么可以清理的,如果确实试图清理那些甚至没有被正确实例化的对象(想想一些指针),它将不必要地引起更多问题. (2认同)

Ric*_*den 14

从构造函数中抛出是可以的,但是你应该确保在main启动之后和完成之前构造你的对象:

class A
{
public:
  A () {
    throw int ();
  }
};

A a;     // Implementation defined behaviour if exception is thrown (15.3/13)

int main ()
{
  try
  {
    // Exception for 'a' not caught here.
  }
  catch (int)
  {
  }
}
Run Code Online (Sandbox Code Playgroud)


Mic*_*hne 6

唯一不会从构造函数抛出异常的情况是,如果您的项目有禁止使用异常的规则(例如,Google不喜欢异常)。在这种情况下,您不会希望在构造函数中使用异常而不是在其他任何地方,而是必须使用某种 init 方法。

  • 有趣的讨论。我个人的观点是,只有在实际设计程序的错误处理结构以利用异常时才应该使用异常。如果您在编写代码后尝试进行错误处理,或者尝试将异常硬塞到不是为它们编写的程序中,它只会导致 try/catch EVERYWHERE(消除异常的优点)或程序在最小的错误。我每天都在处理这两种情况,我不喜欢它。 (5认同)
  • 您可能对 Google 指南的冗长讨论感兴趣,网址为:http://groups.google.com/group/comp.lang.c++.moderated/browse_thread/thread/d7b0a5c663467471/2e5707c44726925a?lnk=gst&amp;q=google#2e5707c44726925a (2认同)

Guy*_*ham 5

添加到这里的所有答案中,我想提到一个非常具体的原因/场景,您可能希望更喜欢从类的Init方法而不是从 Ctor抛出异常(当然,这是首选和更常见的方法)。

我会提前提到,这个例子(场景)假设你不std::unique_ptr为你的类的指针数据成员使用“智能指针”(即- )。

所以关键是:如果你希望你的类的 Dtor 在你(在这种情况下)捕获你的Init()方法抛出的异常之后调用它时“采取行动” - 你不能从 Ctor 抛出异常,导致对 Ctor 的 Dtor 调用不会在“半生不熟”的对象上调用。

请参阅以下示例以证明我的观点:

#include <iostream>

using namespace std;

class A
{
    public:
    A(int a)
        : m_a(a)
    {
        cout << "A::A - setting m_a to:" << m_a << endl;
    }

    ~A()
    {
        cout << "A::~A" << endl;
    }

    int m_a;
};

class B
{
public:
    B(int b)
        : m_b(b)
    {
        cout << "B::B - setting m_b to:" << m_b << endl;
    }

    ~B()
    {
        cout << "B::~B" << endl;
    }

    int m_b;
};

class C
{
public:
    C(int a, int b, const string& str)
        : m_a(nullptr)
        , m_b(nullptr)
        , m_str(str)
    {
        m_a = new A(a);
        cout << "C::C - setting m_a to a newly A object created on the heap (address):" << m_a << endl;
        if (b == 0)
        {
            throw exception("sample exception to simulate situation where m_b was not fully initialized in class C ctor");
        }

        m_b = new B(b);
        cout << "C::C - setting m_b to a newly B object created on the heap (address):" << m_b << endl;
    }

    ~C()
    {
        delete m_a;
        delete m_b;
        cout << "C::~C" << endl;
    }

    A* m_a;
    B* m_b;
    string m_str;
};

class D
{
public:
    D()
        : m_a(nullptr)
        , m_b(nullptr)
    {
        cout << "D::D" << endl;
    }

    void InitD(int a, int b)
    {
        cout << "D::InitD" << endl;
        m_a = new A(a);
        throw exception("sample exception to simulate situation where m_b was not fully initialized in class D Init() method");
        m_b = new B(b);
    }

    ~D()
    {
        delete m_a;
        delete m_b;
        cout << "D::~D" << endl;
    }

    A* m_a;
    B* m_b;
};

void item10Usage()
{
    cout << "item10Usage - start" << endl;

    // 1) invoke a normal creation of a C object - on the stack
    // Due to the fact that C's ctor throws an exception - its dtor
    // won't be invoked when we leave this scope
    {
        try
        {
            C c(1, 0, "str1");
        }
        catch (const exception& e)
        {
            cout << "item10Usage - caught an exception when trying to create a C object on the stack:" << e.what() << endl;
        }
    }

    // 2) same as in 1) for a heap based C object - the explicit call to 
    //    C's dtor (delete pc) won't have any effect
    C* pc = 0;
    try
    {
        pc = new C(1, 0, "str2");
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to create a new C object on the heap:" << e.what() << endl;
        delete pc; // 2a)
    }

    // 3) Here, on the other hand, the call to delete pd will indeed 
    //    invoke D's dtor
    D* pd = new D();
    try
    {
        pd->InitD(1,0);
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to init a D object:" << e.what() << endl;
        delete pd; 
    }

    cout << "\n \n item10Usage - end" << endl;
}

int main(int argc, char** argv)
{
    cout << "main - start" << endl;
    item10Usage();
    cout << "\n \n main - end" << endl;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我会再次提到,这不是推荐的方法,只是想分享一个额外的观点。

此外,正如您从代码中的一些印刷品中看到的那样 - 它基于 Scott Meyers(第 1 版)出色的“更有效的 C++”中的第 10 项。