65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (42投票s)

2010年11月2日

公共领域

5分钟阅读

viewsIcon

64622

downloadIcon

524

使用 C++0x 的一些新功能实现 BOOST_SCOPE_EXIT 的简单外观替代。

目录

简介与背景

本文目标

本文旨在为 Visual C++ 2010(当然也包括 VC++2012 和 2013)介绍一个简洁的作用域卫士,作为 BOOST_SCOPE_EXIT 的替代品,并向初学者解释其实现细节。

根据《更多 C++ 惯用法》,作用域卫士不仅能确保资源释放,还能允许取消释放。因此,严格来说,本文讨论的并非一个完美的作用域卫士。不过,为了方便起见,我简单地称之为“作用域卫士”。

这是使用我的作用域卫士(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 复制到 yp 将被删除两次,程序将导致不可预测的结果。通常,资源清理函数应该只执行一次。因此,最好禁止复制对象。有一个常见的做法可以轻松完成这项工作。我们所要做的就是声明(但不定义)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;
}

在这段代码中,如果没有显式的 deletescope_exit_t 对象将不会被析构。这是矛盾的,因为 RAII idiom 就是为了消除这种显式的资源释放。scope_exit_t 对象应该作为局部变量创建才能正确工作。最好禁止 operator newdelete

我们可以用类似上面的方法来做到这一点。将它们声明为 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; };

……而不是接受已经分配给变量的函数对象。我们如何实现这个功能?在考虑这个问题之前,最好了解一下 lvaluesrvalues 之间的区别。什么是 lvaluesrvalues?尽管这些术语会让初学者头疼,但概念本身很简单。简单来说,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 构造函数接受 lvaluescope_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 日:向源文件存档添加了新版本。

我现在没有时间讨论新版本。以后我会做的。

© . All rights reserved.