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

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

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


当前回答

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

其他回答

除此之外,在你的特定情况下,你不需要从构造函数中抛出,因为如果你的互斥量没有初始化,pthread_mutex_lock实际上会返回一个EINVAL,你可以在调用lock后抛出,就像在std::mutex中所做的那样:

void
lock()
{
  int __e = __gthread_mutex_lock(&_M_mutex);

  // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may)
  if (__e)
__throw_system_error(__e);
}

那么一般来说,对于构造过程中的获取错误,从构造函数抛出是可以的,并且符合RAII(资源获取即初始化)编程范式。

在RAII上检查这个例子

void write_to_file (const std::string & message) {
    // mutex to protect file access (shared across threads)
    static std::mutex mutex;

    // lock mutex before accessing file
    std::lock_guard<std::mutex> lock(mutex);

    // try to open file
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed 1st when leaving scope (regardless of exception)
    // mutex will be unlocked 2nd (from lock destructor) when leaving
    // scope (regardless of exception)
}

关注这些陈述:

静态标准::互斥锁 std::lock_guard<std::mutex> lock(mutex); std::ofstream file(“example.txt”);

第一个陈述是RAII和noexcept。在(2)中很明显,RAII应用于lock_guard,它实际上可以抛出,而在(3)ofstream中似乎不是RAII,因为对象的状态必须通过调用is_open()来检查failbit标志。

乍一看,似乎还没有决定它的标准方式是什么,在第一种情况下std::mutex不抛出初始化,*与OP实现相比*。在第二种情况下,它将抛出std::mutex::lock中抛出的任何东西,而在第三种情况下,根本不抛出任何东西。

注意区别:

(1)可以被声明为静态的,并且实际上将被声明为一个成员变量 (2)实际上永远不会被声明为成员变量 (3)期望被声明为成员变量,并且底层资源可能并不总是可用的。

所有这些形式都是RAII;要解决这个问题,必须分析RAII。

资源:你的对象 获取(分配):正在创建的对象 初始化:你的对象处于不变状态

这并不需要初始化和连接构造中的所有内容。例如,当您要创建一个网络客户端对象时,您不会在创建时将其实际连接到服务器,因为这是一个缓慢的操作,会失败。相反,您可以编写一个connect函数来完成该任务。另一方面,您可以创建缓冲区或仅设置其状态。

Therefore, your issue boils down to defining your initial state. If in your case your initial state is mutex must be initialized then you should throw from the constructor. In contrast it is just fine not to initialize then ( as is done in std::mutex ), and define your invariant state as mutex is created . At any rate the invariant is not compromized necessarily by the state of its member object, since the mutex_ object mutates between locked and unlocked through the Mutex public methods Mutex::lock() and Mutex::unlock().

class Mutex {
private:
  int e;
  pthread_mutex_t mutex_;

public:
  Mutex(): e(0) {
  e = pthread_mutex_init(&mutex_);
  }

  void lock() {

    e = pthread_mutex_lock(&mutex_);
    if( e == EINVAL ) 
    { 
      throw MutexInitException();
    }
    else (e ) {
      throw MutexLockException();
    }
  }

  // ... the rest of your class
};

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

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

从构造函数抛出是可以的,但应该确保这一点 对象是在main启动之后和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)
  {
  }
}

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

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)

如果确实从构造函数抛出异常,请记住,如果需要在构造函数初始化列表中捕获异常,则需要使用函数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
    { ... }