ScopeGuard的使用是否真的能带来更好的代码?

时间:2008-09-07 18:20:44

标签: c++ raii scopeguard

多年前我遇到过Andrei Alexandrescu和Petru Marginean编写的this article,它提出并讨论了一个名为ScopeGuard的实用程序类,用于编写异常安全的代码。我想知道使用这些对象进行编码是否真的能够产生更好的代码,或者是否会混淆错误处理,或许可以在catch块中更好地呈现guard的回调?有没有人有在实际生产代码中使用这些的经验?

8 个答案:

答案 0 :(得分:61)

它肯定会改善您的代码。你的初步声明,它是模糊的,并且代码在{+ 1}}块中的优点在C ++中是不正确的,因为RAII是一个既定的习惯用法。 C ++中的资源处理 由资源获取完成,垃圾收集由隐式析构函数调用完成。

另一方面,显式catch块会使代码膨胀并引入细微错误,因为代码流变得更加复杂,并且必须明确地完成资源处理。

RAII(包括catch s)在C ++中并不是一种模糊的技术,而是坚定地建立了最佳实践。

答案 1 :(得分:29)

如果有一条C ++代码,我建议每个C ++程序员花10分钟学习,那就是ScopeGuard(现在是免费提供的Loki library的一部分)。

我决定尝试使用一个(略微修改过的)ScopeGuard版本来处理我正在研究的小型Win32 GUI程序。您可能知道Win32有许多不同类型的资源需要以不同的方式关闭(例如,内核句柄通常以CloseHandle()关闭,GDI BeginPaint()需要与EndPaint()配对,我使用ScopeGuard来使用所有这些资源,并使用new分配工作缓冲区(例如,用于字符集转换为Unicode或来自Unicode)。

让我感到惊讶的是,更短程序是多少。基本上,这是双赢的:您的代码同时变得更短,更强大。未来的代码更改不能泄漏任何内容。他们只是不能。这有多酷?

答案 2 :(得分:2)

我经常使用它来保护内存使用,从操作系统返回需要释放的内容。例如:

DATA_BLOB blobIn, blobOut;
blobIn.pbData=const_cast<BYTE*>(data);
blobIn.cbData=length;

CryptUnprotectData(&blobIn, NULL, NULL, NULL, NULL, CRYPTPROTECT_UI_FORBIDDEN, &blobOut);
Guard guardBlob=guardFn(::LocalFree, blobOut.pbData);
// do stuff with blobOut.pbData

答案 3 :(得分:1)

我没有使用过这个特殊的模板,但之前我曾经使用过类似的东西。是的,与以不同方式实现的同样健壮的代码相比,它确实可以产生更清晰的代码。

答案 4 :(得分:1)

我认为上述答案缺乏一个重要的注意事项。正如其他人所指出的那样,您可以使用ScopeGuard来释放独立于失败的分配资源(异常)。但这可能不是你唯一想要使用范围保护的东西。事实上,链接文章中的示例使用ScopeGuard用于不同的目的:transcations。简而言之,如果您有多个对象(即使这些对象正确使用RAII),您需要保持一种以某种方式相关的状态,这可能会很有用。如果任何这些对象的状态改变导致异常(我认为,通常意味着它的状态没有改变),那么所有已经应用的改变都需要回滚。这会产生一系列问题(如果回滚失败怎么办?)。您可以尝试推出自己的类来管理这些相关对象,但随着这些增加的数量增加它会变得混乱,您可能会回到内部使用ScopeGuard

答案 5 :(得分:1)

在C ++中,它甚至在D:

中都有特殊的语法
void somefunction() {
    writeln("function enter");
    // c++ has similar constructs but not in syntax level
    scope(exit) writeln("function exit");

    // do what ever you do, you never miss the function exit output
}

答案 6 :(得分:0)

我的经验表明,scoped_guard的使用远不如您可以手工编写的任何可重复使用的简短RAII类。

在尝试scoped_guard之前,我已经编写了RAII类用于

  • 绘制形状后,将GLcolor或GLwidth设置回原始
  • 一旦fclose对文件进行了fopen处理,请确保文件已经被sorting处理。
  • 在执行慢速功能期间将其更改为齿轮/沙漏之后,将鼠标指针重置为其初始状态
  • 将QListView的QListViewItems状态恢复到先前的状态后,一旦我暂时完成了对其class scoped_width { int m_old_width; public: scoped_width(int w) { m_old_width = getGLwidth(); setGLwidth(w); } ~scoped_width() { setGLwidth(m_old_width); } }; void DrawTriangle(Tria *t) { // GLwidth=1 here auto guard = scoped_width(2); // sets GLwidth=2 draw_line(t->a, t->b); draw_line(t->b, t->c); draw_line(t->c, t->a); setGLwidth(5); draw_point(t->a); draw_point(t->b); draw_point(t->c); } // scoped_width sets GLwidth back to 1 here 的修改,我不希望每次更改文本时列表都重新排序一个项目...

使用简单的RAII类

以下是我手工制作的RAII类的代码:

scoped_width

scoped_guard的实现非常简单,并且可重用。 消费者方面也非常简单易读。

使用scoped_guard(C ++ 14)

现在,使用[],我必须捕获引入程序(void DrawTriangle(Tria *t) { // GLwidth=1 here auto guard = sg::make_scoped_guard([w=getGLwidth()](){ setGLwidth(w); }); // capture current GLwidth in order to set it back setGLwidth(2); // sets GLwidth=2 draw_line(t->a, t->b); draw_line(t->b, t->c); draw_line(t->c, t->a); setGLwidth(5); draw_point(t->a); draw_point(t->b); draw_point(t->c); } // scoped_guard sets GLwidth back to 1 here )中的现有值,以便将其传递给后卫的回调:

scoped_guard

以上内容甚至不适用于C ++ 11。 更不用说以这种方式将状态引入lambda会伤害我的眼睛。

使用void DrawTriangle(Tria *t) { // GLwidth=1 here int previous_width = getGLwidth(); // explicitly capture current width auto guard = sg::make_scoped_guard([=](){ setGLwidth(previous_width); }); // pass it to lambda in order to set it back setGLwidth(2); // sets GLwidth=2 draw_line(t->a, t->b); draw_line(t->b, t->c); draw_line(t->c, t->a); setGLwidth(5); draw_point(t->a); draw_point(t->b); draw_point(t->c); } // scoped_guard sets GLwidth back to 1 here (C ++ 11)

在C ++ 11中,您必须这样做:

scoped_guard

如您所见

  • previous_width snoppet要求

    • 3行以保留先前的值(状态)并将其设置为新值,并且
    • 2个堆栈变量(再次为guardRAII class)来保持先前的状态
  • 手工制作的guard需要

    • 1条可读行以设置新状态并保留前一个状态,并且
    • 1个堆栈变量(void some_function() { sg::scoped_guard([](){ cout << "this is printed last"; } cout << "this is printed first"; } )保持先前的状态。

结论

我认为例如

scoped_guard

不能证明scoped_guard有用。

我希望有人能告诉我为什么我没有从scoped_guard获得预期的收益。

我坚信,通过编写简短的手工制作的类,可以比使用更通用但更难使用的{{1}}

更好地利用RAII。

答案 7 :(得分:-1)

我必须说,不,不,不。这里的答案有助于说明为什么它是一个真正可怕的想法。资源处理应该通过可重用的类来完成。他们通过使用范围保护来实现的唯一目的是违反干扰并在他们的代码库中复制他们的资源释放代码,而不是写一个类来处理资源,然后就是这样,对于整个地段。

如果范围保护具有任何实际用途,则资源处理其中之一。在这种情况下,它们大致低于普通RAII,因为RAII是经过重复数据删除的,自动和范围保护是手动代码重复或破坏。