C++11实现模板化(通用化)RAII机制

什么是RAII?

RAII(Resource Acquisition Is Initialization),也称直译为“资源获取就是初始化”,是C++语言的一种避免泄漏的机制。 

RAII的例子

以下是C++11头文件<mutex>中的lock_guard的源代码,看代码注释就清楚了,这是典型的RAII风格。 

    template<typename _Mutex>
    class lock_guard
    {
    public:
      typedef _Mutex mutex_type;

      explicit lock_guard(mutex_type& __m) : _M_device(__m)
      { _M_device.lock(); }//作者注:构造对象时加锁(申请资源),构造函数结束,就可以正常使用资源了

      lock_guard(mutex_type& __m, adopt_lock_t) : _M_device(__m)
      { } // calling thread owns mutex

      ~lock_guard()
      { _M_device.unlock(); }//作者注:析构对象时解锁(释放资源)
      // 作者注:禁用拷贝构造函数
      lock_guard(const lock_guard&) = delete;
      // 作者注:禁用赋值操作符
      lock_guard& operator=(const lock_guard&) = delete;

    private:
      mutex_type&  _M_device;
    };

为了保证lock_guard对象不被错误使用,产生不可预知的后果,上面的代码中删除了拷贝构造函数和赋值运算符,以确保lock_guard不会被复制,这是RAII机制的一个基本特征,后面所有RAII实现都具备这个特性。

RAII模板化实现

利用C++11的新特性(类型推导、右值引用、移动语义、类型萃取、function/bind、lambda表达式等等)写了一个通用化的RAII机制,满足各种类型资源的管理需求。

#include <type_traits>
#include <functional>

namespace gyd
{
    /** 元模板,如果是const类型则去除const修饰符 */
    template<typename T>
    struct no_const
    {
        using type=typename std::conditional<std::is_const<T>::value,
                typename std::remove_const<T>::type,T>::type;
    };
    /**
    * RAII方式管理申请和释放资源的类
    * 对象创建时,执行acquire(申请资源)动作(可以为空函数[]{})
    * 对象析构时,执行release(释放资源)动作
    */
    class raii
    {
    public:
        using fun_type =std::function<void()>;

        explicit raii(fun_type release, fun_type acquire = [] {}, bool com = true) noexcept:
            _commit(com), _release(release)
        {
            acquire();
        }

        ~raii() noexcept
        {
            if (_commit)
                _release();
        }

        ///移动构造函数 允许右值赋值
        raii(raii&& rv)noexcept:_commit(rv._commit),_release(rv._release)
        {
            rv._commit=false;
        }

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

        ///设置_commit标志
        raii& commit(bool c = true)noexcept { _commit = c; return *this; }
    private:
        ///为true时析构函数执行_release
        bool _commit;
    protected:
        /// 析构时执的函数
        std::function<void()> _release;
    }; /* raii */

    ///用于实体资源的raii管理类
    template<typename T>
    class raii_var
    {
    public:
        using    _Self      = raii_var<T>;
        using   acq_type    =std::function<T()>;
        using   rel_type    =std::function<void(T &)>;
        explicit raii_var(acq_type acquire , rel_type release) noexcept:
            _resource(acquire()),_release(release)
        {
            //构造函数中执行申请资源的动作acquire()并初始化resource;
        }
        ///移动构造函数
        raii_var(raii_var&& rv):
            _resource(std::move(rv._resource)),
            _release(std::move(rv._release))
        {
            rv._commit=false;//控制右值对象析构时不再执行_release
        }

        ~raii_var() noexcept
        {
            if (_commit)
                _release(_resource);
        }
        ///设置_commit标志
        _Self& commit(bool c = true)noexcept { _commit = c; return *this; }
        ///获取资源引用
        T& get() noexcept{return _resource;}
        T& operator*() noexcept
        { return get();}

        ///根据 T类型不同选择不同的->操作符模板
        template<typename _T=T>
        typename std::enable_if<std::is_pointer<_T>::value,_T>::type operator->() const noexcept
        { return _resource;}
        template<typename _T=T>
        typename std::enable_if<std::is_class<_T>::value,_T*>::type operator->() const noexcept
        { return std::addressof(_resource);}

    private:
        /* 为true时析构函数执行release */
        bool    _commit=true;
        T   _resource;
        rel_type _release;
    };
    /* 创建 raii 对象,
    * 用std::bind将M_REL,M_ACQ封装成std::function<void()>创建raii对象
    * RES      资源类型
    * M_REL    释放资源的成员函数地址
    * M_ACQ    申请资源的成员函数地址
    */
    template<typename RES, typename M_REL, typename M_ACQ>
    raii make_raii(RES & res, M_REL rel, M_ACQ acq, bool default_com = true)noexcept
    {
        // 编译时检查参数类型
        // 静态断言中用到的is_class,is_member_function_pointer等是用于编译期的计算、
        // 查询、判断、转换的type_traits类,
        // type_traits的详细内容参见:http://www.cplusplus.com/reference/type_traits/
        static_assert(std::is_class<RES>::value, 
            "RES is not a class or struct type.");
        static_assert(std::is_member_function_pointer<M_REL>::value, 
            "M_REL is not a member function.");
        static_assert(std::is_member_function_pointer<M_ACQ>::value,
            "M_ACQ is not a member function.");
        assert(nullptr!=rel&&nullptr!=acq);
        auto p_res=std::addressof(const_cast<typename no_const<RES>::type&>(res));
        return raii(std::bind(rel, p_res), std::bind(acq, p_res), default_com);
    }
    /* 创建 raii 对象 无需M_ACQ的简化版本 */
    template<typename RES, typename M_REL>
    raii make_raii(RES & res, M_REL rel, bool default_com = true)noexcept
    {
        static_assert(std::is_class<RES>::value, 
            "RES is not a class or struct type.");
        static_assert(std::is_member_function_pointer<M_REL>::value, 
            "M_REL is not a member function.");
        assert(nullptr!=rel);
        auto p_res=std::addressof(const_cast<typename no_const<RES>::type&>(res));
        return raii(std::bind(rel, p_res), []{}, default_com);
    }
} /* namespace gyd*/

上面的代码已经在gcc5和vs2015下编译测试通过 gcc编译时需要加上 -std=c++11 编译选项

上面的代码中其实包括了两个类(raii,raii_var)和两个函数(make_raii参数重载),对应着代码提供的三种实现通用RAII机制的方式:

raii是基于可调用对象(Callable Object)来实现的通用RAII机制,直接以可调用对象定义申请资源和释放资源的动作为类初始化参数构造raii对象。适合任何类型(包括非对象资源)资源的RAII管理。
raii_var是实现对于实体资源(非互斥锁)的通用RAII机制模板类。适合实体类(包括非对象资源)资源的RAII管理。
模板函数make_raii在raii基础上做了进一步封装,对于资源对象(struct/class)指定资源对象成员函数分别作为申请资源和释放资源的动作。适用于class类型的资源对象(struct/class)
下面分别对两种方式的用法举例:

以前面的RWLock资源锁为例:

RWLock lock;
void raii_test()
{
    raii guard_r([&](){lock.readUnlock();},[&](){lock.readLock();},true);
    //do something...
}

再举一个文件操作的例子:

#include <fiostream.h>  
int main () 
{  
     ofstream out("out.txt");  
     raii guard_file([&out](){if (out.is_open())out.close();});
     //do something...
 }  

上面的例子中,先打开一个文件,创建一个ofstream 对象,因为在raii构造函数中不需要做任何事,所以raii的构造函数中后面两个参数acquire和default_com都省略使用默认值。只需要在raii对象析构的时候执行关闭文件的动作。

raii_var
上一节中文件操作的例子如果使用raii_var可以这样写:

扫描二维码关注公众号,回复: 4253077 查看本文章
#include <fiostream.h>  
int main () 
{  
    raii_var out([]{return ofstream("out.txt");},
                 [](ofstream &f){if (f.is_open())f.close();});
    auto& f=out.get();//获取ofstream对象;
    //do something....
 }  

举一个jni方面的例子,下面的代码将一个java字节数组转为一个c++的数组结构face_code:

bool jni_utilits::jbytearraytoface_code(JNIEnv *env, jbyteArray bytes,face_code &code) 
{
    if(env->GetArrayLength(bytes)==sizeof(face_code))
    {
        //推导GetByteArrayElements返回的资源对象类型,定义为type
        using type=decltype(env->GetByteArrayElements(bytes, JNI_FALSE));
        raii_var<type> byte_ptr(
                [&]()->type{return env->GetByteArrayElements(bytes, JNI_FALSE);},
                [&](type &r){env->ReleaseByteArrayElements(bytes, r, JNI_ABORT);}
                );
        code=*((face_code*)byte_ptr.get());//获取字节数组指针转为face_code结构
        return true;
    }
    return false;
}

我们知道,获取一个java基本类型数组,要先调用Get<type>ArrayElements获取数据的引用,用完后一定要调用Release<type>ArrayElements释放引用。否则会引起内存泄露。 
上面的代码中把获取和释放的动作封装成了一个raii_var对象byte_ptr,对象构造的时候调用GetByteArrayElements,构造对象之后,调用byte_ptr.get()就可以获取GetByteArrayElements返回的字节数组指针,就可以析构的时候调用ReleaseByteArrayElements

make_raii
为了简化对象类(struct/class)资源的RAII管理机制,所以提供了模板化的make_raii函数。 
下面以RWLock为资源对象说明make_raii的用法:

RWLock lock;
void make_raii_test()
{
    auto guard_read=make_raii(lock,&RWLock::readUnlock,&RWLock::readLock);
    //在这里auto 是C++11中赋予了新含义的关键字,意思是guard_read的类型由编译器自动推算。
    //在VS2015 IDE下鼠标停在guard_read上就能看出,guard_read的实际类型就是
    //raii_class<RWLock,decltype(&RWLock::readUnlock),decltype(&RWLock::readLock)>
    //跟上节例子中guard_read的类型定义完全一样
    //do something...
}

如果只有释放资源(M_REL)的动作,可以使用只有两个参数的版本 
raii make_raii(RES & res, M_REL rel, bool default_com = true)

也正是因为这里要用make_raii构造raii对象再传递给自动变量guard_read,所以在raii中虽然禁止了拷贝构造函数和赋值操作符,却有移动构造函数,就是为了在这里make_raii生成的右值能传递给guard_read,否则不能编译通过。

更进一步简化
如果你觉得上一节的调用方式还是不够简洁,我们可以修改RWLock,添加一个静态成员函数make_guard对make_raii进行便利化封装,进一步隐藏RAII细节。

class RWLock
{
    //other class defineition code....
    static auto make_guard(RWLock &lock)->
    decltype(gdface::make_raii(lock,&RWLock::readUnlock,&RWLock::readLock,true))
    {
        return gdface::make_raii(lock,&RWLock::readUnlock,&RWLock::readLock,true);
    }
    //这里auto xxx -> xxx 的句法使用用了C++11的"追踪返回类型"特性,将返回类型后置,
    //使用decltype关键字推导出返回类型
}

然后我们就可以像这样调用:

RWLock lock;
void make_raii_test()
{
    auto guard=RWLock::make_guard(lock);
    //do something...
}

总结
本文给出了三种RAII使用方法,你可以根据自己需要选择两种方法中的任意一种。 
显然第一种直接构造raii对象的方法更通用,适合于任何类型资源, 
第二种raii_var模板类适用于实体类资源比如打开关闭文件这种acquire动作有返回资源对象的, 
第三种使用make_raii模板函数构造raii对象的方法对于对象型资源(struct/class)更方便。 
如果是自己项目中的资源类对象,再对make_raii进一步封装,使用起来就更方便了。

参考资料
C++中的RAII机制 
异常安全,RAII与C++11 
Type support (basic types, RTTI, type traits) 
支持 C++11/14/17 功能(现代 C++)
--------------------- 
作者:10km 
来源:CSDN 
原文:https://blog.csdn.net/10km/article/details/49847271 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/f110300641/article/details/84312833
今日推荐