我正在与同事讨论如何从构造函数中抛出异常,并且我想要一些反馈。
从设计的角度来看,是否可以从构造函数中抛出异常?
假设我在一个类中包装一个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?这样我就不必为这种低级对象使用异常。
答案 0 :(得分:240)
是的,从失败的构造函数中抛出异常是执行此操作的标准方法。有关详细信息,请阅读有关Handling a constructor that fails的常见问题解答。使用init()方法也可以,但创建互斥对象的每个人都必须记住必须调用init()。我认为这违反了RAII原则。
答案 1 :(得分:95)
如果从构造函数中抛出异常,请记住,如果需要在构造函数初始化列表中捕获该异常,则需要使用函数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
{ ... }
答案 2 :(得分:34)
抛出异常是处理构造函数失败的最佳方法。你应该特别避免半构造一个对象,然后依靠你的类的用户通过测试某种类型的标志变量来检测构造失败。
在一个相关的问题上,你有几种不同的异常类型来处理互斥错误的事实让我有点担心。继承是一个很好的工具,但它可以被过度使用。在这种情况下,我可能更喜欢单个MutexError异常,可能包含信息性错误消息。
答案 3 :(得分:15)
#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
不会调用析构函数,所以如果需要在构造函数中抛出异常,那么很多东西(例如清理?)就可以了。
答案 4 :(得分:14)
从构造函数中抛出是可以的,但是你应该确保这样做 您的对象是在 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)
{
}
}
答案 5 :(得分:4)
如果您的项目通常依赖异常来区分坏数据和良好数据,那么从构造函数中抛出异常比不抛出更好。如果未抛出异常,则在僵尸状态下初始化对象。这样的对象需要暴露一个标志,该标志表示对象是否正确。像这样:
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时它是否正确。这很好地解释了在僵尸状态下初始化对象是一个坏主意。
答案 6 :(得分:4)
除了这个事实,你不需要在特定情况下从构造函数中抛出,因为pthread_mutex_lock
actually returns an EINVAL if your mutex has not been initialized并且你可以在调用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 (资源获取 - 是 - 初始化)编程范例。
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)中ofstream
似乎不是RAII,因为对象状态必须是通过调用检查is_open()
标志的failbit
进行检查。
乍一看,似乎尚未确定标准方式的内容,并且在第一种情况下std::mutex
没有抛出初始化,*与OP实现*相反。在第二种情况下,它将抛出从std::mutex::lock
抛出的任何东西,而在第三种情况下,它根本没有投掷。
注意差异:
(1)可以声明为static,并且实际上将声明为成员变量 (2)实际上永远不会被声明为成员变量 (3)预期被声明为成员变量,并且底层资源可能并不总是可用。
所有这些形式都是 RAII ;要解决这个问题,必须分析 RAII 。
这不需要您初始化并连接构造中的所有内容。例如,当您创建网络客户端对象时,在创建时实际上不会将其连接到服务器,因为它是一个失败的缓慢操作。你会写一个connect
函数来做这件事。另一方面,您可以创建缓冲区或仅设置其状态。
因此,您的问题归结为定义您的初始状态。如果您的初始状态是必须初始化互联网,那么您应该从构造函数中抛出。相比之下,暂时不进行初始化(如在std::mutex
中所做的那样),并在创建互斥锁时定义不变状态。无论如何,不变量不会受到其成员对象状态的影响,因为mutex_
对象通过locked
公共方法{{1}在unlocked
和Mutex
之间发生变异}和Mutex::lock()
。
Mutex::unlock()
答案 7 :(得分:3)
如果您的项目有一个反对使用异常的规则,那么您不会从构造函数中抛出异常的唯一时间(例如,Google不喜欢异常)。在这种情况下,您不希望在构造函数中使用异常,而不是其他任何地方,并且您必须使用某种类型的init方法。
答案 8 :(得分:3)
在这里添加所有答案,我想提到一个非常具体的原因/场景,你可能希望更喜欢从类的Init
方法中抛出异常,而不是从Ctor抛出异常(当然这是首选和更常见的方法。)
我会事先提到这个例子(场景)假设你没有使用&#34;智能指针&#34; (即 - std::unique_ptr
)为您的班级&#39;
s指针数据成员。
所以到了这一点:如果您希望班级的Dtor能够采取行动&#34;当你调用它之后(对于这种情况)你捕获了你的Init()
方法抛出的异常 - 你不能从Ctor抛出异常,因为不会在&#上调用Ctor的Dtor调用34;半生不熟&#34;对象。
请参阅以下示例以证明我的观点:
#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;
}
我将再次提及,这不是推荐的方法,只是想分享一个额外的观点。
另外,正如你可能从代码中的一些印刷品中看到的那样 - 它基于神奇的&#34;更有效的C ++&#34;中的第10项。作者:Scott Meyers(第1版)。
希望它有所帮助。
干杯,
盖。
答案 9 :(得分:-1)