C++ RAII 与 ScopeGuard

C++ RAII 与 ScopeGuard

RAII机制

RAII(Resource Acquisition Is Initialization),也就是“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。

当我们在一个函数内部使用局部变量,当退出了这个局部变量的作用域时,这个变量也就别销毁了;当这个变量是类对象时,这个时候,就会自动调用这个类的析构函数,而这一切都是自动发生的,不要程序员显示的去调用完成。这个也太好了,RAII就是这样去完成的。

由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用,如此,就不用程序员显示的去调用释放资源的操作了。

RAII 过程可以分为四个步骤:
  • 设计一个类封装资源;
  • 在构造函数中初始化;
  • 在析构函数中执行销毁操作;
  • 使用时声明一个该对象的类;

 

先看一个不使用RAII的例子,在下面的UseFile函数中:

void UseFile(char const* fn)
{
    FILE* f = fopen(fn, "r");        // 获取资源
    // 使用资源
    if (!g()) { fclose(f); return; }
    // ...
    if (!h()) { fclose(f); return; }
    // ...
    fclose(f);                       // 释放资源
}

在使用文件f的过程中,因某些操作失败而造成函数提前返回的现象经常出现。这时函数UseFile的执行流程将变为:

现在的问题是:用于释放资源的代码fclose(f)需要在不同的位置重复书写多次。如果再加入异常处理,情况会变得更加复杂。例如,在文件f的使用过程中,程序可能会抛出异常:

void UseFile(char const* fn)
{
    FILE* f = fopen(fn, "r");        // 获取资源
    // 使用资源
    try {
        if (!g()) { fclose(f); return; }
        // ...
        if (!h()) { fclose(f); return; }
        // ...
    }
    catch (...) {
        fclose(f);                   // 释放资源
        throw;
    }
    fclose(f);                       // 释放资源
}

我们必须依靠catch(...)来捕获所有的异常,关闭文件f,并重新抛出该异常。随着控制流程复杂度的增加,需要添加资源释放代码的位置会越来越多。如果资源的数量还不止一个,那么程序员就更加难于招架了。

如果使用RAII 的思想,

class FileHandle {
public:
    FileHandle(char const* n, char const* a) { p = fopen(n, a); }
    ~FileHandle() { fclose(p); }
private:
    // 禁止拷贝操作
    FileHandle(FileHandle const&);
    FileHandle& operator= (FileHandle const&);
    FILE *p;
};

FileHandle类的构造函数调用fopen()获取资源;FileHandle类的析构函数调用fclose()释放资源。请注意,考虑到FileHandle对象代表一种资源,它并不具有拷贝语义,因此我们将拷贝构造函数和赋值运算符声明为私有成员。如果利用FileHandle类的局部对象表示文件句柄资源,那么前面的UseFile函数便可简化为:

void UseFile(char const* fn)
{
    FileHandle file(fn, "r"); 
    // 在此处使用文件句柄f...
    // 超出此作用域时,系统会自动调用file的析构函数,从而释放资源
}

UserFile函数返回时,局部变量file生存期已过,会自动调用其析构函数。

如若使用文件file的代码中有异常抛出,难道析构函数还会被调用吗?此时RAII还能如此奏效吗?问得好。事实上,当一个异常抛出之后,系统沿着函数调用栈,向上寻找catch子句的过程,称为栈辗转开解(stack unwinding)。C++标准规定,在辗转开解函数调用栈的过程中,系统必须确保调用所有已创建起来的局部对象的析构函数。例如:

void Foo()
{
    FileHandle file1("n1.txt", "r"); 
    FileHandle file2("n2.txt", "w");
    Bar();       // 可能抛出异常
    FileHandle file3("n3.txt", "rw")
}

调用Bar()时,局部对象file1和file2已经在Foo的函数调用栈中创建完毕,而file3却尚未创建。如果Bar()抛出异常,那么file2和file1的析构函数会被先后调用(注意:析构函数的调用顺序与构造函数相反);由于此时栈中尚不存在file3对象,因此它的析构函数不会被调用。只有当一个对象的构造函数执行完毕之后,我们才认为该对象的创建工作已经完成。栈辗转开解过程仅调用那些业已创建的对象的析构函数。

 

再看一个多线程同步的例子:

int sum = 0;
std::mutex m;
 
void add()
{   
    m.lock();
    for (int n=0; n<10000; n++) {
        sum++ ;
    }
    m.unlock();
}
 
int main() 
{
    std::thread t1(add);
    std::thread t2(add);
    t1.join();
    t2.join();

    std::cout << sum << std::endl; 
}

 

对锁的管理(lock、unlock)也可以用RAII风格的方式:

int sum = 0;
std::mutex lck;

class MyMutex {
public:
    MyMutex(std::mutex* p): plck(p) {
        plck->lock();
    }
    ~MyMutex() {
        plck->unlock();
    }

private:
    std::mutex* plck;
};
 
void add()
{   
    MyMutex mu(&lck);
    for (int n=0; n<10000; n++) {
        sum++ ;
    }
}
 
int main() 
{
    std::thread t1(add);
    std::thread t2(add);
    t1.join();
    t2.join();

    std::cout << sum << std::endl;
}

事实上,std::lock_guard<T> 就是类似的实现。

 

综上所述,RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。换句话说,拥有对象就等于拥有资源,对象存在则资源必定存在。由此可见,RAII惯用法是进行资源管理的有力武器。

 

 

ScopeGuard

 ScopeGuard 最大的用处也是释放资源。

比如分配内存,做某些操作,再释放内存,很多人会这样写:

void* data = malloc(size);
xxxxx
if (xxx) {
    xxxxx
}
else {
    xxxxx
}

free(data);

这样的代码是很脆弱的,有下面问题。

  • malloc 和 free 可能会相隔很远,难以看出它们的对应关系。
  • 另外中间有任何异常,中途 return,free 都不能被执行,就有资源泄露。
  • 假如将这段代码复用,搬到另外地方,容易漏掉 free。

为解决资源释放问题,有些老旧的 C/C++ 代码,会采用折衷写法,会使用 do {} while(false),甚至会用到 goto。 比如:

do {
    if (xxx) {
        break;
    }

    if (xxx) {
        break;
    }

    xxx 正常操作
    return 1;
} while(false);

free(data);
xxxx 释放资源
return 0;

 

或者

 if (xxxx) {
        goto fail;
    }

    if (xxxx) {
        goto fail;
    }

    xxx 正常操作
    return 1;

fail:
    free(data);
    xxxx 释放资源
    return 0;

 

对比 ScopeGuard 的写法:

void* data = malloc(size);
ON_SCOPE_EXIT {
    free(data);
};

xxxxx
if (xxx) {
    xxxxx
}
else {
    xxxxx
}

资源一旦分配,接下来就立即使用 ScopeGuard 释放资源,这样分配和释放就会靠在一起,当退出作用域的的时候,里面的语句就被执行,释放掉资源。

无论下面的语句抛异常也好,中途退出也好,代码都是安全的。这样的代码也更容易修改,比如将其移动到另外的地方,它还是安全的。假如分配和释放分隔两地,移动代码时就很容易漏掉某些语句。

在Golang语言中,有defer关键字可以实现上面例子中的scope guard,而C++需要自己实现。

 

C++ 实现用到 C++ 11 的 lamda,定义对象将 lamda 存起来,在析构函数中调用。这个 lamda 在不同的场合也有不同的叫法,比如匿名函数,闭包,代码块,block。

有些小地方需要注意:一个是存储 lamda 使用了模板,而不是 std::function, 这个可以避免 lamda 转 std::function 的开销(尽管这个开销在绝大多数情况下可以忽略不计)。

 

#ifndef __SCOPE_GUARD_H__
#define __SCOPE_GUARD_H__

#define __SCOPEGUARD_CONCATENATE_IMPL(s1, s2) s1##s2
#define __SCOPEGUARD_CONCATENATE(s1, s2) __SCOPEGUARD_CONCATENATE_IMPL(s1, s2)

#if defined(__cplusplus)

#include <type_traits>

// ScopeGuard for C++11
namespace clover {
    template <typename Fun>
    class ScopeGuard {
    public:
        ScopeGuard(Fun &&f) : _fun(std::forward<Fun>(f)), _active(true) {
        }

        ~ScopeGuard() {
            if (_active) {
                _fun();
            }
        }

        void dismiss() {
            _active = false;
        }

        ScopeGuard() = delete;
        ScopeGuard(const ScopeGuard &) = delete;
        ScopeGuard &operator=(const ScopeGuard &) = delete;

        ScopeGuard(ScopeGuard &&rhs) : _fun(std::move(rhs._fun)), _active(rhs._active) {
            rhs.dismiss();
        }

    private:
        Fun _fun;
        bool _active;
    };

    namespace detail {
        enum class ScopeGuardOnExit {};

        template <typename Fun>
        inline ScopeGuard<Fun> operator+(ScopeGuardOnExit, Fun &&fn) {
            return ScopeGuard<Fun>(std::forward<Fun>(fn));
        }
    } // namespace detail
} // namespace clover

// Helper macro
#define ON_SCOPE_EXIT \
    auto __SCOPEGUARD_CONCATENATE(ext_exitBlock_, __LINE__) = clover::detail::ScopeGuardOnExit() + [&]()

#else

// ScopeGuard for Objective-C
typedef void (^ext_cleanupBlock_t)(void);
static inline void ext_executeCleanupBlock(__strong ext_cleanupBlock_t *block) {
    (*block)();
}

#define ON_SCOPE_EXIT                                                              \
    __strong ext_cleanupBlock_t __SCOPEGUARD_CONCATENATE(ext_exitBlock_, __LINE__) \
        __attribute__((cleanup(ext_executeCleanupBlock), unused)) = ^

#endif

#endif /* __SCOPE_GUARD_H__ */

 

posted @ 2019-12-05 16:52  如果的事  阅读(2878)  评论(0编辑  收藏  举报