32

我看到了 Andrei Alexandrescu 和 Petru Marginean 多年前写的这篇文章,其中介绍并讨论了一个名为 ScopeGuard 的实用程序类,用于编写异常安全的代码。我想知道使用这些对象进行编码是否真的会导致更好的代码,或者它是否会混淆错误处理,因为也许守卫的回调会更好地呈现在 catch 块中?有没有人有在实际生产代码中使用这些的经验?

4

8 回答 8

62

它肯定会改进您的代码。您暂时提出的主张,即它晦涩难懂,并且代码可以从一个catch块中受益,这在 C++ 中根本不正确,因为 RAII 是一个既定的习惯用法。C++ 中的资源处理通过资源获取来完成的,垃圾回收是通过隐式析构函数调用来完成的。

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

RAII(包括ScopeGuards)在 C++ 中并不是一种晦涩难懂的技术,而是牢固确立的最佳实践。

于 2008-09-07T18:30:18.040 回答
29

是的。

如果有一段 C++ 代码我可以推荐每个 C++ 程序员花 10 分钟学习,那就是 ScopeGuard(现在是免费提供的Loki 库的一部分)。

我决定尝试对我正在开发的小型 Win32 GUI 程序使用(稍作修改)版本的 ScopeGuard。您可能知道 Win32 有许多不同类型的资源需要以不同的方式关闭(例如,内核句柄通常用 关闭CloseHandle(),GDIBeginPaint()需要EndPaint()与工作缓冲区new(例如,用于与 Unicode 的字符集转换)。

令我吃惊的是程序的长度如此之 基本上,这是双赢的:您的代码同时变得更短、更健壮。未来的代码更改不会泄露任何东西。他们就是做不到。多么酷啊?

于 2009-02-15T14:22:26.047 回答
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
于 2008-09-07T21:04:52.193 回答
2

是的。

它在 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
}
于 2016-10-21T04:53:50.480 回答
1

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

于 2008-09-07T18:30:31.487 回答
1

我认为以上答案缺少一个重要说明。正如其他人指出的那样,您可以使用ScopeGuard以释放分配的资源而不受故障(异常)的影响。但这可能不是您可能想要使用范围保护的唯一事情。实际上,链接文章中的示例使用ScopeGuard用于不同的目的:交易。简而言之,如果您有多个对象(即使这些对象正确使用 RAII)需要保持某种相关的状态,这可能会很有用。如果任何这些对象的状态更改导致异常(我认为这通常意味着其状态没有更改),那么所有已应用的更改都需要回滚。这会产生它自己的一系列问题(如果回滚也失败了怎么办?)。您可以尝试推出自己的管理此类相关对象的类,但随着这些相关对象的数量增加,它会变得混乱,并且您可能无论如何都会退回到ScopeGuard内部使用。

于 2016-01-11T08:25:10.940 回答
-1

我不得不说,不,不,它没有。这里的答案有助于证明为什么这是一个真正糟糕的想法。资源处理应该通过可重用的类来完成。他们通过使用范围保护实现的唯一一件事就是违反 DRY 干掉 wazoo 并在整个代码库中复制他们的资源释放代码,而不是编写一个类来处理资源,然后就是这样,全部。

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

于 2015-11-04T18:52:02.837 回答
-2

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

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

  • 一旦我绘制了一个形状,将 GLcolor 或 GLwidth 设置回原来的
  • 确保文件在fclose我编辑后具有 d fopen
  • 在执行慢速函数期间将鼠标指针更改为齿轮/沙漏后,将鼠标指针重置为其初始状态
  • 将 QListView 的状态重置为sorting之前的状态,一旦我暂时完成了更改它的QListViewItems操作——我不希望列表在每次更改单个项目的文本时重新排序...

使用简单的 RAII 类

这是我的代码在我的手工制作的 RAII 类中的样子:

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

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

使用scoped_guard(C++14)

现在,使用scoped_guard,我必须捕获引入者 ( []) 中的现有值,以便将其传递给守卫的回调:

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

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

使用scoped_guard(C++11)

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

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

如你看到的,

  • 代码段scoped_guard需要

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

    • 1 个可读行来设置新状态并保留前一个状态,以及
    • 1 个堆栈变量 ( guard) 来保存先前的状态。

结论

我认为这样的例子

void some_function() {
    sg::scoped_guard([](){ cout << "this is printed last"; }

    cout << "this is printed first";
}

不能证明 的有用性scoped_guard

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

我相信通过编写简短的手工类可以更好地利用 RAII,而不是使用更通用但难以使用的类scoped_guard

于 2020-11-13T15:57:25.283 回答