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

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

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


当前回答

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

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

其他回答

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

是的,从失败的构造函数抛出异常是做到这一点的标准方式。有关更多信息,请阅读有关处理构造函数失败的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
};

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

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

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