从构造函数抛出异常

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

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

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

254829 次浏览

是的,从失败的构造函数抛出异常是做到这一点的标准方式。阅读有关处理失败的构造函数的常见问题以获得更多信息。有init()方法也可以,但是每个创建互斥对象的人都必须记住init()必须被调用。我觉得它违背了RAII原则。

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

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

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

如果确实从构造函数抛出异常,请记住,如果需要在构造函数初始化列表中捕获异常,则需要使用函数try/catch语法。

如。

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
{ ... }
从构造函数抛出是可以的,但你应该确保 你的对象是在主要启动之后和它之前构造的 完成:< / p >
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)
{
}
}

只有当你的项目有禁止使用异常的规则时(例如,谷歌不喜欢异常),你才不会从构造函数抛出异常。在这种情况下,你不希望在构造函数中使用异常,而必须使用某种类型的init方法。

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

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

在你的特定情况下,除了构造函数中的事实是你不需要扔,因为如果互斥量没有初始化,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 (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)
}

关注这些陈述:

  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)预期被声明为成员变量,底层资源可能并不总是可用的

所有这些表单都是RAII;要解决这个问题,必须分析RAII

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

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

因此,你的问题可以归结为定义初始状态。如果在你的情况下你的初始状态是互斥对象必须初始化,那么你应该从构造函数抛出。相反,不初始化(就像在std::mutex中所做的那样),并将你的不变状态定义为互斥被创建就可以了。无论如何,不变量不一定会受到其成员对象状态的影响,因为mutex_对象通过Mutex公共方法Mutex::lock()Mutex::unlock()lockedunlocked之间发生变化。

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

析构函数不被调用,因此如果需要在构造函数中抛出异常,很多东西(例如。打扫?)要做的事。

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

我要提前提到,这个例子(场景)假设你不为你的类使用“智能指针”(即- std::unique_ptr)。

. S指针数据成员

所以说到这一点:如果你希望类的Dtor在你捕捉到Init()方法抛出的异常后(在这种情况下)调用它时“采取行动”-你必须不要从Ctor抛出异常,因为Ctor的Dtor调用不会在“半生”对象上调用。

请看下面的例子来证明我的观点:

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

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

此外,正如您可能已经从代码中的一些打印中看到的那样,它是基于Scott Meyers(第一版)的“更有效的c++”中的第10项。

注意the destructor never gets called after the exception is thrown from the constructor

struct B
{
char* p;
B() {
cout << "Constructor - B" << endl;
p = new char[1024];
throw std::exception("some exception");
}
~B() { // NEVER GETS CALLED AFTER EXCEPTION !!!! - memory leak
cout << "Destructor - B" << endl;
delete[] p;
}
};


int main()
{
try {
B b;
}
catch (...) {
cout << "Catch called " << endl;
}
}

输出:

Constructor - B
Catch called       (Note: B's Destructor is NEVER called)