Visual C++ 2010 的简单外观范围保护






4.89/5 (42投票s)
使用 C++0x 的一些新功能实现 BOOST_SCOPE_EXIT 的简单外观替代。
目录
简介与背景
本文目标
本文旨在为 Visual C++ 2010(当然也包括 VC++2012 和 2013)介绍一个简洁的作用域卫士,作为 BOOST_SCOPE_EXIT 的替代品,并向初学者解释其实现细节。
这是使用我的作用域卫士(SCOPE_EXIT
宏)的真实示例,是 WTL 程序 WinMain()
函数的一部分。
CComModule _Module;
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
if (FAILED(::CoInitialize(nullptr)))
return 0;
SCOPE_EXIT { ::CoUninitialize(); };
ULONG_PTR gdipToken;
Gdiplus::GdiplusStartupInput gsi;
if (Gdiplus::GdiplusStartup(&gdipToken, &gsi, nullptr) != Gdiplus::Ok)
return 0;
SCOPE_EXIT { Gdiplus::GdiplusShutdown(gdipToken); };
if (FAILED(_Module.Init(nullptr, hInstance)))
return 0;
SCOPE_EXIT { _Module.Term(); };
// Some work.
return 0;
// Executed in the reverse order.
// 1. _Module.Term();
// 2. Gdiplus::GdiplusShutdown(gdipToken);
// 3. ::CoUninitialize();
}
SCOPE_EXIT
宏的执行顺序与它们出现的顺序相反。这是因为 C++ 语言保证局部变量的析构顺序与构造顺序相反。
您是否觉得它比下面的代码更简洁?
关于 BOOST_SCOPE_EXIT
BOOST_SCOPE_EXIT
是 Boost 库中的一组宏。它实现了 C++ 中所谓的作用域卫士。它非常有用,但使用起来有点痛苦。尽管我通常鼓励我的同事将 Boost 应用到我们的工作中,但它的一些缺点阻止了我推荐它。我有两点想要改进。首先,它太显眼了。其次,它不支持空捕获列表。看看这个与上面等价的示例。
CComModule _Module;
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
if (FAILED(::CoInitialize(nullptr)))
return 0;
int dummy;
BOOST_SCOPE_EXIT((&dummy)) {
::CoUninitialize();
} BOOST_SCOPE_EXIT_END;
ULONG_PTR gdipToken;
Gdiplus::GdiplusStartupInput gsi;
if (Gdiplus::GdiplusStartup(&gdipToken, &gsi, nullptr) != Gdiplus::Ok)
return 0;
BOOST_SCOPE_EXIT((&gdipToken)) {
Gdiplus::GdiplusShutdown(gdipToken);
} BOOST_SCOPE_EXIT_END;
if (FAILED(_Module.Init(nullptr, hInstance)))
return 0;
BOOST_SCOPE_EXIT((&_Module)) {
_Module.Term();
} BOOST_SCOPE_EXIT_END;
// Some work.
return 0;
// Executed in the reverse order.
// 1. _Module.Term();
// 2. Gdiplus::GdiplusShutdown(gdipToken);
// 3. ::CoUninitialize();
}
我们不得不在代码块的前后都放置两个显眼的符号。尽管 CoUninitialize()
这样的函数应该是主角,但配角却抢了风头。那么,什么是“多余的”呢?宏至少需要一个参数,所以我们不得不放置一个不必要的参数。我们可以使用已有的变量,如 hInstance
,而不是一个特别声明的变量,但这可能会让我们更加困惑。
然而,Visual C++ 2010 发布时包含了一些 C++0x 的特性。新引入的特性,尤其是 lambda 表达式,启发了我改进这些缺点。
工作原理
简陋的实现
首先,我以一种简陋的方式实现了作用域卫士,作为进一步解释的基础。
作用域卫士是一种特殊的 RAII。典型的 RAII 类在其构造函数中分配资源,并在其析构函数中释放资源。与典型的 RAII 类不同,作用域卫士通过构造函数接受用户定义的任何函数,并在其析构函数中执行这些函数。因此,最少的实现如下所示:
#include <iostream>
#include <functional>
class scope_exit_t
{
typedef std::function<void()> func_t;
public:
scope_exit_t(const func_t &f) : func(f) {}
~scope_exit_t() { func(); }
private:
const func_t func;
};
int main()
{
std::cout << "Resource Allocation." << std::endl;
scope_exit_t x = []{ std::cout << "Resource Deallocation" << std::endl; };
std::cout << "Some Work." << std::endl;
return 0;
}
// Output:
// Resource Allocation.
// Some Work.
// Resource Deallocation
尽管它很简单,但它向我们展示了 C++0x 带来的一些主要改进。std::function<T>
类型和 lambda 表达式使代码如此之短。那么,让我们来解决一些遗留的问题。
禁止复制
看看这段代码:
int main()
{
int *p = new int[10];
scope_exit_t x = [&]{ delete[] p; };
scope_exit_t y = x;
// Some work.
return 0;
}
scope_exit_t
对象从 x
复制到 y
。p
将被删除两次,程序将导致不可预测的结果。通常,资源清理函数应该只执行一次。因此,最好禁止复制对象。有一个常见的做法可以轻松完成这项工作。我们所要做的就是声明(但不定义)private
的复制构造函数和 operator =
。
// Inside the scope_exit_t
private:
scope_exit_t(const scope_exit_t &);
const scope_exit_t& operator=(const scope_exit_t &);
危险的代码将不再能够编译。
(如果您可以使用 Boost,那么继承 boost::noncopyable 来禁止复制会更容易。)
禁止 Operator new & delete
operator new
也很麻烦。
int main()
{
scope_exit_t *p = new scope_exit_t([]{ std::cout << 1 << std::endl; });
// Some work.
delete p; // You should never skip it!
return 0;
}
在这段代码中,如果没有显式的 delete
,scope_exit_t
对象将不会被析构。这是矛盾的,因为 RAII idiom 就是为了消除这种显式的资源释放。scope_exit_t
对象应该作为局部变量创建才能正确工作。最好禁止 operator new
和 delete
。
我们可以用类似上面的方法来做到这一点。将它们声明为 private
函数。
// Inside the scope_exit_t
private:
void *operator new(size_t);
void *operator new[](size_t);
void operator delete(void *);
void operator delete[](void *);
接受右值,拒绝左值
下一个问题在这里显示:
int main()
{
std::function<void()> f = []{ std::cout << 1 << std::endl; }; // Function 1.
scope_exit_t x = f;
f = []{ std::cout << 2 << std::endl; }; // Function 2.
return 0;
}
// Output:
// 1
哪个函数会运行?用户除非检查 scope_exit_t
的实现,否则无法回答这个问题。非专业人士甚至在检查之后可能仍然会感到困惑。这太含糊了。为了避免这种含糊不清,理想情况下 scope_exit_t
应该直接接受 lambda 表达式,如下所示……
scope_exit_t x = []{ std::cout << 1 << std::endl; };
……而不是接受已经分配给变量的函数对象。我们如何实现这个功能?在考虑这个问题之前,最好了解一下 lvalues
和 rvalues
之间的区别。什么是 lvalues
和 rvalues
?尽管这些术语会让初学者头疼,但概念本身很简单。简单来说,lvalues
是分配给变量的值,而 rvalues
则不是。
// 1: The integer is lvalue. It's assigned to a variable.
int x = 1;
std::cout << x << std::endl;
// 2: The integer is rvalue. It's not assigned to any variables.
std::cout << 2 << std::endl;
// 3: The lambda expression is lvalue. It's assigned to a variable.
std::function<void()> f = []{ std::cout << 3 << std::endl; };
scope_exit_t y = f;
// 4: The lambda expression is rvalue. It's not assigned to any variables.
// It looks like assigned to z, but it's directly passed
// to the constructor of scope_exit_t.
scope_exit_t z = []{ std::cout << 4 << std::endl; };
这个例子告诉我们,为了达到我们的目标,我们应该拒绝 lvalues
。这得益于 C++0x 中称为“Rvalue
引用”的功能。
#include <iostream>
// Takes lvalue references (the argument has one ampersand.)
void func(int &x) { std::cout << "Called by lvalue: x == " << x << std::endl; };
// Takes rvalue references (the argument has two ampersands.)
void func(int &&x) { std::cout << "Called by rvalue: x == " << x << std::endl; };
int main()
{
int x = 1;
func(x); // Call by lvalue.
func(2); // Call by rvalue.
return 0;
}
// Output:
// Called by lvalue: x == 1
// Called by rvalue: x == 2
在这段代码中,有两个 func()
的重载。第二个重载您可能不熟悉。它接受一个 rvalue
引用。这是 C++ 程序员目前最热门的话题,但本文并不是要参与其中。我只想让您注意到,调用哪个重载取决于参数是 lvalue
还是 rvalue
。这使得函数能够判断它们接收的是哪种类型的值。因此,我们应该让 public
构造函数接受 rvalue
,而 private
构造函数接受 lvalue
。scope_exit_t
将不再能从分配给变量的函数对象构造。
// Inside the scope_exit_t
public:
scope_exit_t(func_t &&f) : func(f) {} // Accept rvalue references.
private:
scope_exit_t(func_t &); // Reject lvalue references.
隐藏在宏之后
在某些情况下,scope_exit_t
类变量需要唯一的名称。但我们不必每次都给它们命名。我们可以将这项工作交给预处理器。 预定义宏 __COUNTER__ 允许我们生成唯一的变量名。(虽然它不是标准的,但 g++ 也可以使用。)但我们需要一个小的技巧(但这是常见做法)来正确使用它。这向您展示了这种做法。
#define SCOPE_EXIT_CAT2(x, y) x##y
#define SCOPE_EXIT_CAT1(x, y) SCOPE_EXIT_CAT2(x, y)
#define SCOPE_EXIT scope_exit_t SCOPE_EXIT_CAT1(scope_exit_, __COUNTER__) = [&]
在这段代码中,SCOPE_EXIT_CAT1
看起来什么都没做,但它起着重要的作用。如果缺少 SCOPE_EXIT_CAT1
,__COUNTER__
就不会被替换成数字,因为 __COUNTER__
在被替换之前就被连接到了 scope_exit_
。SCOPE_EXIT_CAT1
是为了延迟令牌连接所必需的。
(如果您可以使用 Boost,那么使用 BOOST_PP_CAT 来连接令牌会更容易。)
历史
- 2010 年 11 月 3 日:首次发布
- 2014 年 6 月 3 日:向源文件存档添加了新版本。
我现在没有时间讨论新版本。以后我会做的。