我熟悉RAII的优点,但我最近在这样的代码中遇到了问题:
class Foo
{
public:
Foo()
{
DoSomething();
...
}
~Foo()
{
UndoSomething();
}
}
一切都很好,除了构造函数...
部分中的代码引发了异常,结果是UndoSomething()
从未被调用过。
有一些明显的方法来解决这个特定的问题,比如在{/ 1}}的try / catch块中包裹...
,但是a:那是重复的代码,b:try / catch块是我尝试使用RAII技术避免的代码味道。而且,如果涉及多个Do / Undo对,代码可能会变得更糟,更容易出错,而且我们必须在中途清理。
我想知道有更好的方法可以做到这一点 - 也许一个单独的对象需要一个函数指针,并在它被反演时调用该函数?
UndoSomething()
我知道不会编译,但它应该显示原则。 Foo然后变成......
class Bar
{
FuncPtr f;
Bar() : f(NULL)
{
}
~Bar()
{
if (f != NULL)
f();
}
}
请注意,foo现在不需要析构函数。这听起来比它的价值更麻烦吗,或者这已经是一种常见的模式,有助于我处理繁重的工作?
答案 0 :(得分:30)
问题是你的班级试图做太多。 RAII的原理是它获取资源(在构造函数中或稍后),析构函数释放它;该类仅用于管理该资源。
在您的情况下,DoSomething()
和UndoSomething()
以外的任何内容都应由班级用户负责,而不是班级本身。
正如Steve Jessop在评论中所说:如果你有多个资源需要获取,那么每个资源都应该由自己的RAII对象管理;将这些聚合为另一个依次构造每个类的数据成员可能是有意义的。然后,如果任何收购失败,所有先前获得的资源将由各个班级成员的析构者自动释放。
(另外,请记住Rule of Three;您的班级需要阻止复制或以合理的方式实施,以防止多次调用UndoSomething()
。)
答案 1 :(得分:17)
只需将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
}
}
答案 2 :(得分:6)
我也会用RAII来解决这个问题:
class Doer
{
Doer()
{ DoSomething(); }
~Doer()
{ UndoSomething(); }
};
class Foo
{
Doer doer;
public:
Foo()
{
...
}
};
doer是在ctor体启动之前创建的,当析构函数通过异常失败或者对象被正常销毁时会被破坏。
答案 3 :(得分:6)
你的班级太多了。将DoSomething / UndoSomething移动到另一个类('Something'),并将该类的对象作为Foo类的一部分,因此:
class Foo
{
public:
Foo()
{
...
}
~Foo()
{
}
private:
class Something {
Something() { DoSomething(); }
~Something() { UndoSomething(); }
};
Something s;
}
现在,在调用Foo的构造函数时调用DoSomething,如果Foo的构造函数抛出,则UndoSomething会被正确调用。
答案 4 :(得分:6)
try / catch一般来说不代码气味,应该用来处理错误。在你的情况下,它会是代码味道,因为它不处理错误,只是清理。这就是破坏者的用途。
(1)如果构造函数失败时应该调用析构函数中的所有,只需将其移动到私有清理函数(由析构函数调用)和构造函数(如果失败) 。这似乎就是你已经完成的事情。干得好。
(2)一个更好的想法是:如果有多个do / undo对可以单独破坏,那么它们应该被包装在他们自己的小RAII类中,这就是它的微型任务,并在它自身之后进行清理。我不喜欢你当前给它一个可选的清理指针函数的想法,这只是令人困惑。清理应始终与初始化配对,这是RAII的核心概念。
答案 5 :(得分:0)
经验法则:
第三条规则的示例包括std::shared_ptr
,std::unique_ptr
,scope_guard
,std::vector<>
,std::list<>
,scoped_lock
,当然还有{ {1}}以下课程。
附录
你可以走得那么远,写点东西与C风格的东西互动:
Trasher
输出:
#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
永远不会运行,但你的init / delete对是。
一个好处是:如果你的init函数本身抛出,你的delete函数就不会被调用,因为init-function抛出的任何异常直接通过~Foo()
,因此Trasher()
不会被执行。
注意:最重要的是~Trasher()
,否则标准不需要堆栈展开。