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

Does ScopeGuard use really lead to better code?

提问人:SCFrench 提问时间:9/8/2008 最后编辑:Aakash GoelSCFrench 更新时间:11/13/2020 访问量:12703

问:

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

C++ RAII ScopeGuard

评论

1赞 Lilian A. Moraru 3/16/2013
C++0x/C++11 现在用“shared_ptr”来做到这一点。
0赞 Lilian A. Moraru 3/16/2013
我确实看到它给了你更多的力量。数据库的例子非常好。仅使用 shared_ptr它将调用析构函数,该析构函数通常只关闭连接,而使用 ScopedGuard,您实际上可以在发生异常时回滚......

答:

63赞 Konrad Rudolph 9/8/2008 #1

它肯定会改进您的代码。你试探性地声称它是晦涩难懂的,代码可以从块中脱颖而出,这在 C++ 中根本不正确,因为 RAII 是一个既定的习语。C++ 中的资源处理是通过资源获取完成的,垃圾回收是通过隐式析构函数调用完成的。catch

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

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

评论

6赞 odinthenerd 2/18/2013
+1 坚持现代 C++ 技术。还应该注意的是,Alexandrescu在这里展示了ScopeGuard的新版本:channel9.msdn.com/Shows/Going+Deep/...,它更容易使用。Micht 值得一一编辑。
1赞 Leon Timmermans 9/8/2008 #2

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

2赞 1800 INFORMATION 9/8/2008 #3

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

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
30赞 j_random_hacker 2/15/2009 #4

是的。

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

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

令我惊讶的是,该计划竟然短了多少。基本上,这是双赢的:您的代码同时变得更短、更健壮。将来的代码更改不会泄露任何内容。他们就是做不到。这有多酷?

-1赞 Puppy 11/5/2015 #5

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

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

3赞 j_kubik 1/11/2016 #6

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

2赞 timepp 10/21/2016 #7

是的。

它在 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
}
-3赞 Grim Fandango 11/13/2020 #8

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

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

  • 一旦我绘制了一个形状,将 GLcolor 或 GLwidth 设置回原始形状
  • 确保一个文件在我编辑后有 d。fclosefopen
  • 将鼠标指针重置为其初始状态,在执行慢速函数期间将其更改为 gears/hourgrlass 之后
  • 将QListView的状态重置回以前的状态,一旦我暂时完成了更改它 - 我不希望每次更改单个项目的文本时列表都会重新排序...sortingQListViewItems

使用简单的 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

使用 (C++14)scoped_guard

现在,使用 ,我必须捕获 introducer () 中的现有值,以便将其传递给守卫的回调: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 会伤害我的眼睛。

使用 (C++11)scoped_guard

在 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

正如你所看到的,

  • Snoppet 需要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

评论

1赞 Bojan Hrnkas 7/25/2023
scoped_width用法是可怕和危险的。您可以使用两个不同的命令来设置宽度(scoped_width(2) 和 setGLwidth(5)),并依靠注释来解释代码的作用。