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

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

假设我在一个类中包装了一个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值?这样我就不必为如此低级的对象使用异常了。


当前回答

注意,从构造函数抛出异常后,析构函数永远不会被调用。

struct B
{
    char* p;
    B() { 
        cout << "Constructor - B" << endl; 
        p = new char[1024];
        throw std::exception("some exception");
    }
    ~B() { // NEVER GETS CALLED AFTER EXCEPTION !!!! - memory leak 
        cout << "Destructor - B" << endl; 
        delete[] p;
    } 
};

int main()
{
    try {
        B b;
    }
    catch (...) {
        cout << "Catch called " << endl;
    }
}

输出:

Constructor - B
Catch called       (Note: B's Destructor is NEVER called)

其他回答

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

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

e.g.

func::func() : foo()
{
    try {...}
    catch (...) // will NOT catch exceptions thrown from foo constructor
    { ... }
}

vs.

func::func()
    try : foo() {...}
    catch (...) // will catch exceptions thrown from foo constructor
    { ... }

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

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时,它是否正确工作。这很好地解释了在僵尸状态下初始化对象是一个坏主意。

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

#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;
}

输出:

heap: new foo
bar() called
foo() called
heap exception: throw something
stack: foo
bar() called
foo() called
stack exception: throw something

析构函数不被调用,因此如果需要在构造函数中抛出异常,很多东西(例如。打扫?)要做的事。