我正在与一位同事就构造函数抛出异常进行辩论,并认为我需要一些反馈。

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

假设我将 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的回归?这样我就不必对如此低级别的对象使用异常。

有帮助吗?

解决方案

是的,从失败的构造函数中抛出异常是执行此操作的标准方法。有关更多信息,请阅读有关处理失败的构造函数的常见问题解答。使用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
    { ... }

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

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

#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

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

从构造函数中抛出是可以的,但是你应该确保这样做 您的对象是在 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) { ... };
}

使用此解决方案,您可以获取状态标志,作为工厂方法的返回值,而无需输入包含错误数据的构造函数。

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

除了 事实上你不需要扔 在您的具体情况下来自构造函数,因为 pthread_mutex_lock 实际上返回一个 有效值 如果你的互斥体还没有初始化 你可以在调用后抛出 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)
}

重点关注这些陈述:

  1. static std::mutex mutex
  2. std::lock_guard<std::mutex> lock(mutex);
  3. std::ofstream file("example.txt");

第一个语句是 RAII 和 noexcept. 。在 (2) 中,很明显 RAII 应用于 lock_guard 它实际上可以 throw ,而在(3)中 ofstream 似乎不是 RAII ,因为必须通过调用来检查对象状态 is_open() 检查 failbit 旗帜。

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

注意差异:

(1)可以将静态声明为静态,实际上将被宣布为成员变量(2)实际上永远不会被预期为成员变量(3),将被宣布为成员变量,而基础资源可能会被宣布为成员变量(3)并非总是可用。

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

  • 资源:你的对象
  • 获取(分配):你反对被创建
  • 初始化:你的对象在它的 不变状态

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

因此,您的问题归结为定义您的初始状态。如果在你的情况下你的初始状态是 互斥体必须初始化 那么你应该从构造函数中抛出。相反,不初始化就可以了(就像在 std::mutex ),并将不变状态定义为 互斥体已创建 。无论如何,不​​变量不一定会因其成员对象的状态而受到损害,因为 mutex_ 对象在之间发生变异 lockedunlocked 通过 Mutex 公共方法 Mutex::lock()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
};

如果您的项目有反对使用例外的规则,那么您不会从构造函数中抛出异常的唯一时间(例如, Google 不喜欢例外情况。在这种情况下,您不希望在构造函数中使用异常,而不是其他任何地方,并且您必须使用某种类型的init方法。

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

我事先会提到这个例子(场景)假定你不使用<!>智能指针<!> (即std::unique_ptr)为你的班级' s指针数据成员。

所以要点:如果您希望班级的Dtor <!>“采取行动<!>”;当你调用它之后(对于这种情况)你捕获你的Init()方法抛出的异常 - 你必须不从Ctor抛出异常,因为不会在<!>上调用Ctor的Dtor调用;半成品<!> QUOT;对象。

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

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

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

另外,正如你可能从代码中的一些打印中看到的那样 - 它基于神奇的<!>中的第10项;更有效的C ++ <!>;作者:Scott Meyers(第1版)。

希望它有所帮助。

干杯,

盖。

虽然我没有在专业级别上使用C ++,但在我看来,可以从构造函数中抛出异常。我在.Net中这样做(如果需要)。查看

scroll top