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

发布于 2024-07-04 11:28:10 字数 256 浏览 4 评论 0原文

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

I came across this article written by Andrei Alexandrescu and Petru Marginean many years ago, which presents and discusses a utility class called ScopeGuard for writing exception-safe code. I'd like to know if coding with these objects truly leads to better code or if it obfuscates error handling, in that perhaps the guard's callback would be better presented in a catch block? Does anyone have any experience using these in actual production code?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

无边思念无边月 2024-07-11 11:28:10

是的。

它在 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
}

Yes.

It was so important in C++ that even a special syntax for it in 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
}
动听の歌 2024-07-11 11:28:10

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

I haven't used this particular template but I've used something similar before. Yes, it does lead to clearer code when compared to equally robust code implemented in different ways.

后eg是否自 2024-07-11 11:28:10

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

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

I have to say, no, no it does not. The answers here help to demonstrate why it's a genuinely awful idea. Resource handling should be done through re-usable classes. The only thing they've achieved by using a scope guard is to violate DRY up the wazoo and duplicate their resource freeing code all over their codebase, instead of writing one class to handle the resource and then that's it, for the whole lot.

If scope guards have any actual uses, resource handling is not one of them. They're massively inferior to plain RAII in that case, since RAII is deduplicated and automatic and scope guards are manual code duplication or bust.

巴黎盛开的樱花 2024-07-11 11:28:10

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

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

  • 将 GLcolor 或 GLwidth 设置回原始值,一旦我绘制了形状,
  • 请确保文件已 fclosed 一旦我已经fopen编辑了它。
  • 在执行慢速函数期间将鼠标指针更改为 gears/hourgrlass 之后,将鼠标指针重置为其初始状态
  • 一旦我将 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 snoppet 需要

    • 3 行用于保留先前的值(状态)并将其设置为新值,并且
    • 2 个堆栈变量(同样是 previous_widthguard)来保存先前的状态
  • 手工制作的RAII类需要

    • 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

My experience shows that usage of scoped_guard is far inferior to any of the short reusable RAII classes that you can write by hand.

Before trying the scoped_guard, I had written RAII classes to

  • set GLcolor or GLwidth back to the original, once I've drawn a shape
  • make sure a file has fclosed once I had fopened it.
  • reset a mouse pointer to its initial state, after I've changed it to gears/hourgrlass during a execution of a slow function
  • reset the sorting state of a QListView's back to its previous state, once I've temporarily finished with altering its QListViewItems -- I did not want the list to reorder itself everytime I changed the text of a single item...

using simple RAII class

Here's how my code looked like with my hand-crafted RAII classes:

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

Very simple implementation for scoped_width, and quite reusable.
Very simple and readable from the consumer side, also.

using scoped_guard (C++14)

Now, with the scoped_guard, I have to capture the existing value in the introducer ([]) in order to pass it to the guard's callback:

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

The above doesn't even work on C++11.
Not to mention that trying to introduce the state to the lambda this way hurts my eyes.

using scoped_guard (C++11)

In C++11 you have to do this:

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

As you can see,

  • the scoped_guard snoppet requires

    • 3 lines to keep previous value (state) and set it to a new one, and
    • 2 stack variables (previous_width and guard, again) to hold the previous state
  • the hand-crafted RAII class requires

    • 1 readable line to set new state and keep the previous one, and
    • 1 stack variable (guard) to hold the previous state.

Conclusion

I think that examples such as

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

    cout << "this is printed first";
}

are no proof of the usefullness of scoped_guard.

I hope that somebody can show me why I don't get the expected gain from scoped_guard.

I am convinced that RAII can be exploited better by writing short hand-crafted classes, than using the more generic but hard to use scoped_guard

梦忆晨望 2024-07-11 11:28:10

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

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

I often use it for guarding memory usage, things that need to be freed that were returned from the OS. For example:

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
笑着哭最痛 2024-07-11 11:28:10

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

I think above answers lack one important note. As others have pointed out, you can use ScopeGuard in order to free allocated resources independent of failure (exception). But that might not be the only thing you might want to use scope guard for. In fact, the examples in linked article use ScopeGuard for a different purpose: transcations. In short, it might be useful if you have multiple objects (even if those objects properly use RAII) that you need to keep in a state that's somehow correlated. If change of state of any of those objects results in an exception (which, I presume, usually means that its state didn't change) then all changes already applied need to be rolled back. This creates it's own set of problems (what if a rollback fails as well?). You could try to roll out your own class that manages such correlated objects, but as the number of those increases it would get messy and you would probably fall back to using ScopeGuard internally anyway.

-柠檬树下少年和吉他 2024-07-11 11:28:10

它肯定会改进你的代码。 您暂时提出的主张(它很晦涩并且代码值得使用 catch 块)在 C++ 中根本不正确,因为 RAII 是一种既定的习惯用法。 C++ 中的资源处理是通过资源获取来完成的,垃圾收集是通过隐式析构函数调用来完成的。

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

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

It definitely improves your code. Your tentatively formulated claim, that it's obscure and that code would merit from a catch block is simply not true in C++ because RAII is an established idiom. Resource handling in C++ is done by resource acquisition and garbage collection is done by implicit destructor calls.

On the other hand, explicit catch blocks would bloat the code and introduce subtle errors because the code flow gets much more complex and resource handling has to be done explicitly.

RAII (including ScopeGuards) isn't an obscure technique in C++ but firmly established best-practice.

梦归所梦 2024-07-11 11:28:10

是的。

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

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

让我惊讶的是程序的程度。基本上,这是双赢的:您的代码变得更短,同时更健壮。 未来的代码更改不会泄漏任何内容。 他们就是不能。 多么酷啊?

Yes.

If there is one single piece of C++ code that I could recommend every C++ programmer spend 10 minutes learning, it is ScopeGuard (now part of the freely available Loki library).

I decided to try using a (slightly modified) version of ScopeGuard for a smallish Win32 GUI program I was working on. Win32 as you may know has many different types of resources that need to be closed in different ways (e.g. kernel handles are usually closed with CloseHandle(), GDI BeginPaint() needs to be paired with EndPaint(), etc.) I used ScopeGuard with all these resources, and also for allocating working buffers with new (e.g. for character set conversions to/from Unicode).

What amazed me was how much shorter the program was. Basically, it's a win-win: your code gets shorter and more robust at the same time. Future code changes can't leak anything. They just can't. How cool is that?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文