如何处理 RAII 的构造函数失败
How to handle constructor failure for RAII
我熟悉 RAII 的优点,但我最近在代码中绊倒了一个问题,如下所示:
class Foo
{
public:
Foo()
{
DoSomething();
...
}
~Foo()
{
UndoSomething();
}
}
一切都很好,除了构造函数...
部分中的代码抛出异常,结果UndoSomething()
从未被调用。
有一些明显的方法可以解决该特定问题,例如将...
包装在 try/catch 块中,然后调用 UndoSomething()
,但 a:这是重复代码,b:try/catch 块是我尝试使用 RAII 技术避免的代码异味。而且,如果涉及多个 Do/Undo 对,代码可能会变得更糟且更容易出错,我们必须中途清理。
我想知道有更好的方法来做到这一点 - 也许一个单独的对象接受一个函数指针,并在它反过来被破坏时调用该函数?
class Bar
{
FuncPtr f;
Bar() : f(NULL)
{
}
~Bar()
{
if (f != NULL)
f();
}
}
我知道这不会编译,但它应该显示原理。然后,Foo变成了...
class Foo
{
Bar b;
Foo()
{
DoSomething();
b.f = UndoSomething;
...
}
}
请注意,foo 现在不需要析构函数。这听起来像是比它的价值更麻烦,还是这已经是一种常见的模式,在 boost 中有一些有用的东西来为我处理繁重的工作?
问题是你的类试图做太多。RAII 的原则是它获取资源(在构造函数中或以后),析构函数释放它;该类的存在只是为了管理该资源。
在您的情况下,除DoSomething()
和UndoSomething()
之外的任何内容都应该是类用户的责任,而不是类本身。
正如Steve Jessop在评论中所说:如果你有多个资源要获取,那么每个资源都应该由自己的RAII对象管理;将这些资源聚合为另一个类的数据成员可能是有意义的,该类依次构造每个资源。然后,如果任何获取失败,所有以前获取的资源将由各个类成员的析构函数自动释放。
(另外,请记住三法则;您的类需要防止复制,或者以某种合理的方式实现它,以防止多次调用UndoSomething()
)。
只需将DoSomething
/UndoSomething
变成正确的RAII句柄:
struct SomethingHandle
{
SomethingHandle()
{
DoSomething();
// nothing else. Now the constructor is exception safe
}
SomethingHandle(SomethingHandle const&) = delete; // rule of three
~SomethingHandle()
{
UndoSomething();
}
}
class Foo
{
SomethingHandle something;
public:
Foo() : something() { // all for free
// rest of the code
}
}
我也将使用RAII解决此问题:
class Doer
{
Doer()
{ DoSomething(); }
~Doer()
{ UndoSomething(); }
};
class Foo
{
Doer doer;
public:
Foo()
{
...
}
};
执行者是在 ctor 主体启动之前创建的,当析构函数通过异常失败或对象正常销毁时,该操作者会被销毁。
你的一个类中有太多的东西。 将 DoSomething/UndoSomething 移动到另一个类("Something"),并将该类的对象作为类 Foo 的一部分,因此:
class Foo
{
public:
Foo()
{
...
}
~Foo()
{
}
private:
class Something {
Something() { DoSomething(); }
~Something() { UndoSomething(); }
};
Something s;
}
现在,在调用 Foo 的构造函数时已经调用了 DoSomething,如果 Foo 的构造函数抛出,那么 UndoSomething 就会被正确调用。
try/catch通常不是代码异味,它应该用于处理错误。 但是,在您的情况下,这将是代码异味,因为它不是处理错误,只是清理。 这就是析构函数的用途。
(1) 如果在构造函数失败时应调用析构函数中的所有内容,只需将其移动到私有清理函数,该函数由析构函数调用,并在失败时由构造函数调用。 这似乎是你已经做过的事情。 干得好。
(2)一个更好的想法是:如果有多个可以单独破坏的执行/撤消对,则应将它们包装在自己的小RAII类中,该类执行小型任务,并在自身清理之后进行清理。我不喜欢您目前给它一个可选的清理指针功能的想法,这令人困惑。 清理应始终与初始化配对,这是 RAII 的核心概念。
经验法则:
- 如果您的类手动管理某些内容的创建和删除,则它做得太多了。
- 如果你的类手动编写了复制作业/构造,它可能管理太多了
- 例外情况:唯一目的是管理一个实体的类
第三条规则的例子是std::shared_ptr
、std::unique_ptr
、scope_guard
、std::vector<>
、std::list<>
、scoped_lock
,当然还有下面的Trasher
类。
补遗。
你可以走得更远,写一些东西来与C风格的东西进行交互:
#include <functional>
#include <iostream>
#include <stdexcept>
class Trasher {
public:
Trasher (std::function<void()> init, std::function<void()> deleter)
: deleter_(deleter)
{
init();
}
~Trasher ()
{
deleter_();
}
// non-copyable
Trasher& operator= (Trasher const&) = delete;
Trasher (Trasher const&) = delete;
private:
std::function<void()> deleter_;
};
class Foo {
public:
Foo ()
: meh_([](){std::cout << "hello!" << std::endl;},
[](){std::cout << "bye!" << std::endl;})
, moo_([](){std::cout << "be or not" << std::endl;},
[](){std::cout << "is the question" << std::endl;})
{
std::cout << "Fooborn." << std::endl;
throw std::runtime_error("oh oh");
}
~Foo() {
std::cout << "Foo in agony." << std::endl;
}
private:
Trasher meh_, moo_;
};
int main () {
try {
Foo foo;
} catch(std::exception &e) {
std::cerr << "error:" << e.what() << std::endl;
}
}
输出:
hello!
be or not
Fooborn.
is the question
bye!
error:oh oh
因此,~Foo()
永远不会运行,但您的初始化/删除对会运行。
一件好事是:如果你的 init 函数本身抛出,你的删除函数不会被调用,因为 init 函数抛出的任何异常都会直接通过Trasher()
,因此~Trasher()
不会被执行。
注意:重要的是有一个最外层的try/catch
,否则,标准不要求堆叠放卷。
- "error: no matching function for call to"构造函数错误
- C++17复制构造函数,在std::unordereded_map上进行深度复制
- 如果C++类在类方法中具有动态分配,但没有构造函数/析构函数或任何非静态成员,那么它仍然是POD类型吗
- 为什么在没有显式默认构造函数的情况下,将另一个结构封装在联合中作为成员的结构不能编译
- 为什么在C++中使用私有复制构造函数与删除复制构造函数
- 选择要调用的构造函数
- 如何委托派生类使用其父构造函数?
- 构造函数正在调用一个使用当前类类型的函数
- 避免在构造函数中分配或保持简单性(和 RAII?
- 如果RAII构造函数抛出呢
- C++:使用RAII解析构造函数-初始化器列表依赖项
- 如何处理 RAII 的构造函数失败
- RAII 在两个构造函数之间进行选择的方式
- RAII 多个构造函数
- 构造函数中的 RAII 和异常
- 在具有移动语义的RAII类中,默认构造函数应该做什么
- 当构造函数抛出异常时,RAII是如何工作的
- 通过构造函数和析构函数实现 RAII 是否被认为是糟糕的"现代C++"?
- 当混合默认构造函数和非默认构造函数时,RAII是如何工作的
- 捕获构造函数异常的RAII方法