我正在与一位同事就从构造函数中抛出异常进行辩论,并认为我想要一些反馈。
从设计的角度来看,可以从构造函数中抛出异常吗?
假设我在一个类中包装了一个 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
的返回返回一个布尔值?这样我就不必为这样一个低级别的对象使用异常。
std::lock_guard
。
lock
和 unlock
以便您的互斥锁类型与 std::lock_guard
一起使用;他在这里重新实现 std::mutex
,而不是 std::lock_guard
,这两个类在 C++ 标准库中是分开的是有原因的。
是的,从失败的构造函数中抛出异常是执行此操作的标准方法。阅读有关 Handling a constructor that fails 的常见问题解答以了解更多信息。有一个 init() 方法也可以,但是每个创建 mutex 对象的人都必须记住必须调用 init()。我觉得这违反了RAII原则。
如果确实从构造函数中抛出异常,请记住,如果需要在构造函数初始化程序列表中捕获该异常,则需要使用函数 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
{ ... }
抛出异常是处理构造函数失败的最好方法。您应该特别避免半构建对象,然后依靠您的类的用户通过测试某种标志变量来检测构造失败。
在相关的一点上,你有几种不同的异常类型来处理互斥错误,这让我有点担心。继承是一个很好的工具,但它可能被过度使用。在这种情况下,我可能更喜欢单个 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
不调用析构函数,因此如果需要在构造函数中抛出异常,则需要做很多事情(例如清理?)。
bar* b
替换为 std::unique_ptr<bar> b
(您必须删除 delete b;
并添加 <memory>
标头),然后再次运行。
valgrind --leak-check=full ./a.out
抱怨块丢失:错误摘要:来自 2 个上下文的 2 个错误
从构造函数中抛出是可以的,但是您应该确保在 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)
{
}
}
唯一不会从构造函数抛出异常的情况是您的项目有禁止使用异常的规则(例如,Google 不喜欢异常)。在这种情况下,您不会比其他任何地方都更希望在构造函数中使用异常,而您必须使用某种 init 方法。
在这里添加所有答案,我想提一下,一个非常具体的原因/场景,您可能希望从类的 Init
方法而不是从 Ctor 抛出异常(这当然是首选和更常见的方法)。
我将提前提到,这个示例(场景)假设您不为类的指针数据成员使用“智能指针”(即 std::unique_ptr
)。
直截了当:如果您希望您的类的 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(第 1 版)出色的“更有效的 C++”中的第 10 项。
如果您的项目通常依赖异常来区分坏数据和好数据,那么从构造函数中抛出异常是比不抛出更好的解决方案。如果没有抛出异常,则将对象初始化为僵尸状态。这样的对象需要暴露一个标志,表明该对象是否正确。像这样的东西:
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。这是对错误的呼吁——没有什么比在继续之前忘记测试条件更简单的了。
如果构造函数抛出异常,构造对象的实体应该立即处理问题。下游的对象消费者可以自由地假设对象是 100% 可操作的,因为他们获得了它。
这种讨论可以在许多方向上继续进行。
例如,使用异常作为验证问题是一种不好的做法。一种方法是结合工厂类的 Try 模式。如果您已经在使用工厂,请编写两种方法:
class ScalerFactory
{
public:
Scaler CreateScaler(double factor) { ... }
int TryCreateScaler(double factor, Scaler **scaler) { ... };
}
使用此解决方案,您可以就地获取状态标志,作为工厂方法的返回值,而无需输入带有错误数据的构造函数。
第二件事是如果你用自动化测试覆盖代码。在这种情况下,每段使用不抛出异常的对象的代码都必须通过一个额外的测试来覆盖——当 IsValid() 方法返回 false 时它是否正确运行。这很好地解释了在僵尸状态下初始化对象是一个坏主意。
除了 您不需要在特定情况下从构造函数中抛出之外,因为 pthread_mutex_lock
actually returns an EINVAL if your mutex has not been initialized 并且您可以像在 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(资源获取是初始化)编程范式。
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)
}
关注这些陈述:
静态 std::mutex 互斥 std::lock_guard
第一个语句是 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
函数来执行此操作。另一方面,您可以创建缓冲区或仅设置其状态。
因此,您的问题归结为定义您的初始状态。如果在你的情况下你的初始状态是 mutex must be initialized 那么你应该从构造函数中抛出。相比之下,不进行初始化就好了(就像在 std::mutex
中所做的那样),并将您的不变状态定义为 mutex is created 。无论如何,不变量不一定会受到其成员对象的状态的影响,因为 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
};
请注意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)