我正在与一位同事讨论从构造函数抛出异常的问题,我想我需要一些反馈。

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

假设我在一个类中包装了一个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_;
};

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

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


当前回答

虽然我没有在专业水平上使用过c++,但在我看来,从构造函数抛出异常是可以的。我在. net中这样做(如果需要的话)。看看这个和这个链接。你可能会感兴趣。

其他回答

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

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

因此,关键是:如果你希望类的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;
}

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

此外,正如您可能已经从代码中的一些打印中看到的那样,它是基于Scott Meyers(第一版)的“更有效的c++”中的第10项。

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

如果您的项目通常依赖异常来区分坏数据和好数据,那么从构造函数抛出异常比不抛出更好。如果没有抛出异常,则对象将初始化为僵尸状态。这样的对象需要公开一个标志,说明该对象是否正确。就像这样:

class Scaler
{
    public:
        Scaler(double factor)
        {
            if (factor == 0)
            {
                _state = 0;
            }
            else
            {
                _state = 1;
                _factor = factor;
            }
        }

        double ScaleMe(double value)
        {
            if (!_state)
                throw "Invalid object state.";
            return value / _factor;
        }

        int IsValid()
        {
            return _status;
        }

    private:
        double _factor;
        int _state;

}

这种方法的问题在于调用方。类的每个用户在实际使用对象之前都必须执行一个if。这是对bug的呼吁——没有什么比在继续之前忘记测试一个条件更简单的了。

如果从构造函数抛出异常,构造对象的实体应该立即处理问题。下游的对象消费者可以自由地假设对象是100%可操作的,因为他们获得了对象。

这个讨论可以在很多方面继续下去。

例如,将异常用作验证是一种糟糕的实践。一种方法是将Try模式与工厂类结合使用。如果你已经在使用工厂,那么写两个方法:

class ScalerFactory
{
    public:
        Scaler CreateScaler(double factor) { ... }
        int TryCreateScaler(double factor, Scaler **scaler) { ... };
}

使用此解决方案,您可以就地获得状态标志,作为工厂方法的返回值,而无需使用坏数据进入构造函数。

第二件事是如果你用自动化测试覆盖代码。在这种情况下,使用不抛出异常的object的每段代码都必须包含一个额外的测试——当IsValid()方法返回false时,它是否正确工作。这很好地解释了在僵尸状态下初始化对象是一个坏主意。

是的,从失败的构造函数抛出异常是做到这一点的标准方式。有关更多信息,请阅读有关处理构造函数失败的FAQ。有init()方法也可以,但是每个创建互斥对象的人都必须记住init()必须被调用。我觉得这违背了RAII原则。

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

在相关的一点上,您有几种不同的异常类型来处理互斥错误,这一点让我有点担心。继承是一个很好的工具,但它可能被过度使用。在这种情况下,我可能更喜欢一个muterror异常,可能包含一个信息丰富的错误消息。