抛出构造函数的exception
我正在和同事讨论有关从构造函数中抛出exception的问题,我想我想得到一些反馈。
从devise的angular度来看,从构造函数中抛出exception是可以的?
假设我在课堂上包装了一个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调用失败,互斥对象不可用,所以抛出exception可确保不会创build互斥锁。
我应该为Mutex类创build一个成员函数init,然后调用pthread mutex_init,在这个基础上返回一个基于pthread mutex_init返回的bool? 这样我不必为这样的低级对象使用exception。
是的,从失败的构造函数中抛出exception是这样做的标准方法。 阅读关于处理失败的构造函数的更多信息。 有一个init()方法也可以,但是每个创build互斥对象的人都必须记住init()必须被调用。 我觉得这违背了RAII的原则。
如果您从构造函数中抛出exception,请记住,如果您需要在构造函数初始值设定项列表中捕获该exception,则需要使用函数try / catch语法。
例如
func::func() : foo() { try {...} catch (...) // will NOT catch exceptions thrown from foo constructor { ... } }
与
func::func() try : foo() {...} catch (...) // will catch exceptions thrown from foo constructor { ... }
抛出exception是处理构造函数失败的最好方法。 你应该特别避免半build构一个对象,然后依靠你的类的用户通过testing某种标志variables来检测构build失败。
在一个相关的问题上,你有几个不同的exceptiontypes来处理互斥错误,这让我担心。 inheritance是一个伟大的工具,但它可以被过度使用。 在这种情况下,我可能会更喜欢单个MutexErrorexception,可能包含一个信息错误消息。
抛出你的构造函数是可以的,但是你应该确保你的对象是在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) { } }
#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
析构函数没有被调用,所以如果一个exception需要在构造函数中抛出,那么很多东西(比如清理?)都要做。
如果您的项目通常依赖于exception来区分不良数据和良好数据,那么抛出构造函数的exception比不抛出更好。 如果exception不被抛出,则对象被初始化为僵尸状态。 这样的对象需要暴露一个标志,说明对象是否正确。 像这样的东西:
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。 这是一个错误的呼唤 – 没有比在继续之前忘记testing条件更简单的了。
在从构造函数中抛出exception的情况下,构造对象的实体应该立即处理问题。 对象消费者可以自由地假定对象是从他们获得它的事实中100%操作的。
这个讨论可以继续在很多方面。
例如,将exception用作validation是一个不好的做法。 一种方法是与工厂类一起使用Try模式。 如果你已经在使用工厂,那么写两个方法:
class ScalerFactory { public: Scaler CreateScaler(double factor) { ... } int TryCreateScaler(double factor, Scaler **scaler) { ... }; }
使用这个解决scheme,您可以在原地获取状态标志,作为工厂方法的返回值,而不用input带有错误数据的构造函数。
第二件事是,如果你正在用自动化testing覆盖代码。 在这种情况下,每一段使用不引发exception的对象的代码将不得不用一个额外的testing来覆盖 – 当IsValid()方法返回false时它是否正确运行。 这很好地解释了在僵尸状态下初始化对象是一个坏主意。
除了你不需要抛出构造函数,因为pthread_mutex_lock
实际上返回一个EINVAL,如果你的互斥体没有被初始化,并且你可以在std::mutex
完成lock
之后抛出:
void lock() { int __e = __gthread_mutex_lock(&_M_mutex); // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may) if (__e) __throw_system_error(__e); }
那么一般来说, 从施工人员的施工投资中就可以得到施工期间的采集错误,并符合RAII (Resource-acquisition-is-Initialization)编程范例。
在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) }
重点关注这些陈述:
-
static std::mutex mutex
-
std::lock_guard<std::mutex> lock(mutex);
-
std::ofstream file("example.txt");
第一个说法是RAII和noexcept
。 在(2)中很明显,RAII被应用在lock_guard
,它实际上可以throw
,而(3)中的stream似乎不是RAII,因为对象状态必须通过调用检查failbit
标志的is_open()
来检查。
乍一看,它似乎没有确定它的标准方式 ,在第一种情况下, std::mutex
不会抛出初始化,*与OP实现*相反。 在第二种情况下,它会抛出从std::mutex::lock
抛出的任何东西,而在第三种情况下根本就没有抛出。
注意不同之处:
(1)可以被声明为静态的,并且实际上将被声明为一个成员variables(2)永远不会被期望被声明为一个成员variables(3)被期望被声明为一个成员variables,而底层资源可能并不总是可用的。
所有这些forms都是RAII ; 要解决这个问题,必须分析RAII 。
- 资源:你的对象
- 获取(分配):你创build的对象
- 初始化:你的对象处于不变状态
这并不要求你初始化和连接所有的东西。 例如,当您创build一个networking客户端对象时,创build时实际上不会将其连接到服务器,因为这是一个缓慢的操作失败。 你会写一个connect
函数来做到这一点。 另一方面,您可以创build缓冲区或只是设置其状态。
因此,你的问题归结为定义你的初始状态。 如果在你的情况下,你的初始状态是互斥体,必须初始化,那么你应该从构造函数中抛出。 相反,它只是没有初始化(就像在std::mutex
所做的那样),并且在创build互斥锁时定义您的不变状态。 无论如何,不variables不会因其成员对象的状态而受到损害,因为mutex_
对象通过Mutex
公共方法Mutex::lock()
和Mutex::unlock()
在locked
和unlocked
之间变异。
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 };
唯一不会从构造函数中抛出exception的情况是,如果您的项目有一个反对使用exception的规则(例如, Google不喜欢exception)。 在这种情况下,你不希望在你的构造函数中使用exception,而只能使用某种types的init方法。
虽然我没有在专业级别上使用C ++,但我认为,从构造函数中抛出exception是可以的。 我在.Net中这样做(如果需要的话)。 看看这个和这个链接。 这可能是你的兴趣。