Throwing exceptions from constructors
我正在和一个同事讨论如何抛出构造函数的异常,我想我需要一些反馈。
从设计的角度来看,是否可以从构造函数中抛出异常?
假设我正在类中包装一个posix互斥体,它看起来像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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_; }; |
我的问题是,这是标准的方法吗?因为如果
我应该为mutex类创建一个成员函数init并调用
是的,从失败的构造函数中抛出异常是执行此操作的标准方法。有关如何处理失败的构造函数的详细信息,请阅读此常见问题解答。也可以使用init()方法,但创建mutex对象的每个人都必须记住必须调用init()。我觉得这违反了RAII原则。
如果确实从构造函数引发异常,请记住,如果需要在构造函数初始值设定项列表中捕获该异常,则需要使用函数try/catch语法。
例如
1 2 3 4 5 6 | func::func() : foo() { try {...} catch (...) // will NOT catch exceptions thrown from foo constructor { ... } } |
VS
1 2 3 4 | func::func() try : foo() {...} catch (...) // will catch exceptions thrown from foo constructor { ... } |
抛出异常是处理构造函数失败的最佳方法。您应该特别避免半构造一个对象,然后依靠类的用户通过测试某种类型的标志变量来检测构造失败。
在一个相关的点上,您有几个不同的异常类型来处理互斥错误,这一事实让我有点担心。继承是一个很好的工具,但它可以被过度使用。在这种情况下,我可能更喜欢一个mutexerrror异常,可能包含一个信息性错误消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | #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; } |
输出:
1 2 3 4 5 6 7 8 | heap: new foo bar() called foo() called heap exception: throw something stack: foo bar() called foo() called stack exception: throw something |
不会调用析构函数,因此如果需要在构造函数中抛出异常,则会有很多东西(例如清理?)去做。
可以从构造函数中抛出,但应该确保您的对象是在main启动之后和之前构造的完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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) { } } |
除了在特定情况下不需要抛出构造函数之外,因为如果您的互斥体尚未初始化,并且您可以像在
1 2 3 4 5 6 7 8 9 | void lock() { int __e = __gthread_mutex_lock(&_M_mutex); // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may) if (__e) __throw_system_error(__e); } |
然后,一般来说,对于构造过程中的获取错误,从构造函数抛出是可以的,并且符合RAII(资源获取是初始化)编程范式。
在raii上检查这个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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) } |
关注这些陈述:
第一个声明是raii和
乍一看,它似乎还没有决定它的标准方式是什么,在第一种情况下,与OP实现相比,
注意区别:
(1)可以声明为静态,并将实际声明为成员变量(2)永远不会实际期望声明为成员变量(3)应该声明为成员变量,并且基础资源可能并不总是可用的。
所有这些形式都是RAII;要解决这个问题,必须分析RAII。
- 资源:您的对象
- 获取(分配):正在创建的对象
- 初始化:对象处于其不变状态
这不需要初始化和连接构造上的所有内容。例如,当您要创建一个网络客户机对象时,您不会在创建时将其实际连接到服务器,因为这是一个缓慢的操作,但会出现故障。您可以编写一个
因此,您的问题归根结底就是定义初始状态。如果您的初始状态是mutex,则必须初始化它,然后从构造函数抛出。相反,如果不初始化then(就像在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 }; |
如果您的项目通常依赖异常来区分坏数据和好数据,那么从构造函数中抛出异常比不抛出更好的解决方案。如果未引发异常,则对象将在僵尸状态下初始化。此类对象需要公开一个标志,该标志指示对象是否正确。像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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模式。如果你已经在使用工厂,那么写两种方法:
1 2 3 4 5 6 | class ScalerFactory { public: Scaler CreateScaler(double factor) { ... } int TryCreateScaler(double factor, Scaler **scaler) { ... }; } |
使用此解决方案,您可以就地获取状态标志,作为工厂方法的返回值,而不必使用错误数据输入构造函数。
第二件事是,如果用自动化测试覆盖代码。在这种情况下,使用不引发异常的对象的每一段代码都必须被一个额外的测试覆盖——当isvalid()方法返回false时,它是否正确工作。这很好地解释了在僵尸状态下初始化对象是一个坏主意。
我想说的是,除了这里的所有答案之外,还有一个非常具体的原因/场景,您可能希望从类的
我将提前提到这个示例(场景)假设您没有为您的类使用"智能指针"(即-
因此,关键是:如果您希望类的dtor在(在本例中)捕获
请参阅下面的示例来演示我的观点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | #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 <<" item10Usage - end" << endl; } int main(int argc, char** argv) { cout <<"main - start" << endl; item10Usage(); cout <<" main - end" << endl; return 0; } |
我会再次提到,这不是推荐的方法,只是想分享一个额外的观点。
此外,正如你可能从代码中的一些打印中看到的那样,它是基于Scott Meyers(第一版)中的"更有效的C++"中的第10项。
希望它有帮助。
干杯,
家伙。
唯一一次您不会从构造函数中抛出异常的是,如果您的项目有一个规则禁止使用异常(例如,Google不喜欢异常)。在这种情况下,您不希望在构造函数中使用异常,而应该使用某种类型的init方法。
虽然我还没有在专业层面上使用C++,但在我看来,从构造函数中抛出异常是可以的。我在.NET中这样做(如果需要)。看看这个和这个链接。这可能是你感兴趣的。