我看到了 Andrei Alexandrescu 和 Petru Marginean 多年前写的这篇文章,其中介绍并讨论了一个名为 ScopeGuard 的实用程序类,用于编写异常安全的代码。我想知道使用这些对象进行编码是否真的会导致更好的代码,或者它是否会混淆错误处理,因为也许守卫的回调会更好地呈现在 catch 块中?有没有人有在实际生产代码中使用这些的经验?
8 回答
它肯定会改进您的代码。您暂时提出的主张,即它晦涩难懂,并且代码可以从一个catch
块中受益,这在 C++ 中根本不正确,因为 RAII 是一个既定的习惯用法。C++ 中的资源处理是通过资源获取来完成的,垃圾回收是通过隐式析构函数调用来完成的。
另一方面,显式catch
块会使代码膨胀并引入细微的错误,因为代码流变得更加复杂并且必须显式地完成资源处理。
RAII(包括ScopeGuard
s)在 C++ 中并不是一种晦涩难懂的技术,而是牢固确立的最佳实践。
是的。
如果有一段 C++ 代码我可以推荐每个 C++ 程序员花 10 分钟学习,那就是 ScopeGuard(现在是免费提供的Loki 库的一部分)。
我决定尝试对我正在开发的小型 Win32 GUI 程序使用(稍作修改)版本的 ScopeGuard。您可能知道 Win32 有许多不同类型的资源需要以不同的方式关闭(例如,内核句柄通常用 关闭CloseHandle()
,GDIBeginPaint()
需要EndPaint()
与工作缓冲区new
(例如,用于与 Unicode 的字符集转换)。
令我吃惊的是程序的长度如此之短。 基本上,这是双赢的:您的代码同时变得更短、更健壮。未来的代码更改不会泄露任何东西。他们就是做不到。多么酷啊?
我经常使用它来保护内存使用情况,即从操作系统返回的需要释放的东西。例如:
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
是的。
它在 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
}
我没有使用过这个特定的模板,但我以前使用过类似的东西。是的,与以不同方式实现的同样健壮的代码相比,它确实会产生更清晰的代码。
我认为以上答案缺少一个重要说明。正如其他人指出的那样,您可以使用ScopeGuard
以释放分配的资源而不受故障(异常)的影响。但这可能不是您可能想要使用范围保护的唯一事情。实际上,链接文章中的示例使用ScopeGuard
用于不同的目的:交易。简而言之,如果您有多个对象(即使这些对象正确使用 RAII)需要保持某种相关的状态,这可能会很有用。如果任何这些对象的状态更改导致异常(我认为这通常意味着其状态没有更改),那么所有已应用的更改都需要回滚。这会产生它自己的一系列问题(如果回滚也失败了怎么办?)。您可以尝试推出自己的管理此类相关对象的类,但随着这些相关对象的数量增加,它会变得混乱,并且您可能无论如何都会退回到ScopeGuard
内部使用。
我不得不说,不,不,它没有。这里的答案有助于证明为什么这是一个真正糟糕的想法。资源处理应该通过可重用的类来完成。他们通过使用范围保护实现的唯一一件事就是违反 DRY 干掉 wazoo 并在整个代码库中复制他们的资源释放代码,而不是编写一个类来处理资源,然后就是这样,全部。
如果范围保护有任何实际用途,那么资源处理就不是其中之一。在这种情况下,它们大大不如普通的 RAII,因为 RAII 是重复数据删除和自动的,范围保护是手动代码复制或破坏。
我的经验表明,使用 ofscoped_guard
远不如您可以手写的任何简短的可重用 RAII 类。
在尝试之前scoped_guard
,我已经编写了 RAII 类来
- 一旦我绘制了一个形状,将 GLcolor 或 GLwidth 设置回原来的
- 确保文件在
fclose
我编辑后具有 dfopen
。 - 在执行慢速函数期间将鼠标指针更改为齿轮/沙漏后,将鼠标指针重置为其初始状态
- 将 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_width
和guard
,再次)保持先前的状态
手工制作
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