前言
什么是RAII?
RAII(Resource Acquisition Is Initialization),也称直译为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的机制。 C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。 RAII 机制就是利用了C++的上述特性,在需要获取使用资源RES的时候,构造一个临时对象(T),在其构造T时获取资源,在T生命期控制对RES的访问使之始终保持有效,最后在T析构的时候释放资源。以达到安全管理资源对象,避免资源泄漏的目的。
正文
为什么要使用RAII? 那么我们经常所说的资源是如何定义的?说到资源,我们立刻能想到的就是内存啊,文件句柄等等啊,但只有这些吗? 对于资源的概念不要想象的太狭隘,在计算机系统中,资源是个定义宽泛的概念,所有数量有限且对系统正常运行具有一定作用的元素都是资源。比如:网络套接字、互斥锁、文件句柄、内存、数据库记录等等,它们属于系统资源。由于系统的资源是有限的,就好比自然界的石油,铁矿一样,不是取之不尽,用之不竭的。 所以,我们在编程使用系统资源时,都必须遵循一个步骤:
1.申请资源; 2.使用资源; 3.释放资源。
第一步和第三步缺一不可,因为资源必须要申请才能使用的,使用完成以后,必须要释放,如果不释放的话,就会造成资源泄漏。
一, 介绍左值引用, 右值引用, 移动语义和完美转发
参考 “从4行代码看右值引用” 博客地址:https://www.cnblogs.com/qicosmos/p/4283455.html
博主写非常经典
这边做一个笔记
-
- 左值引用定义
int i = 0;
int& j = i;
这里的int&是对左值进行绑定(但是int&却不能绑定右值),相应的,对右值进行绑定的引用就是右值引用, 他的语法是这样的A&&,通过双引号来表示绑定类型为A的右值。通过&&我们就可以很方便的绑定右值了,比如我 们可以这样绑定一个右值:
-
- 介绍右值引用定义
int&& i = 0;
两个问题,第一个问题就是临时对象非必要的昂贵的拷贝操作,第二个问题是在模板函数中如何按照参数的 实际类型进行转发。通过引入右值引用,很好的解决了这两个问题,改进了程序性能,后面将会详细介绍右值引 用是如何解决这两个问题的。
右值引用相关的概念比较多 比如:右值、纯右值、将亡值、universal references、引用折叠、移动语义、move语义和完美转发等等。很多都是新概念
1, 第1行代码的故事
int i = getVar();
上面的这行代码很简单,从getVar()函数获取一个整形值,然而,这行代码会产生几种类型的值呢? 答案是会产生两种类型的值,一种是左值i,一种是函数getVar()返回的临时值,这个临时值在表达式结束后就销毁了, 而左值i在表达式结束后仍然存在,这个临时值就是右值,具体来说是一个纯右值,右值是不具名的。
区分左值和右值的一个简单办法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。
所有的具名变量或对象都是左值,而匿名变量则是右值,比如,简单的赋值语句:
int i = 0;
在这条语句中,i 是左值,0 是字面量,就是右值。在上面的代码中,i 可以被引用,0 就不可以了。具体来说上面 的表达式中等号右边的0是纯右值(prvalue),在C++11中所有的值必属于左值、将亡值、纯右值三者之一。比如,非引用 返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值。而将亡值是C++11新增的、与右 值引用相关的表达式,比如,将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值等。
关于将亡值我们会在后面介绍,先看下面的代码:
int j = 5;
auto f = []{return 5;};
上面的代码中5是一个原始字面量, []{return 5;}是一个lambda表达式,都是属于纯右值,他们的特点是在表达式结束之后就销毁了。
通过地行代码我们对右值有了一个初步的认识,知道了什么是右值,接下来再来看看第二行代码。
2, 第2行代码的故事
T&& k = getVar();
第二行代码和第一行代码很像,只是相比第一行代码多了”&&”,他就是右值引用,我们知道左值引用是对左值的引用,那么,对应的, 对右值的引用就是右值引用,而且右值是匿名变量,我们也只能通过引用的方式来获取右值。虽然第二行代码和第一行代码看起来差别不大, 但是实际上语义的差别很大,这里,getVar()产生的临时值不会像第一行代码那样,在表达式结束之后就销毁了,而是会被“续命”, 他的生命周期将会通过右值引用得以延续,和变量k的声明周期一样长。
① 右值引用的第一个特点:
通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着, 该右值临时量将会一直存活下去。让我们通过一个简单的例子来看看右值的生命周期。
事例:
#include <iostream>
//using namespace std;
int g_constructCount=0;
int g_copyConstructCount=0;
int g_destructCount=0;
struct A
{
A(){
std::cout << "construct: " << ++g_constructCount << std::endl;
}
A(const A& a)
{
std::cout << "copy construct: " << ++g_copyConstructCount << std::endl;
}
~A()
{
std::cout << "destruct: " << ++g_destructCount << std::endl;
}
};
A GetA()
{
return A();
}
int main() {
A a = GetA();
return 0;
}
输出结果:
construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
为了清楚的观察临时值,在使用g++编译时设置编译选项-fno-elide-constructors用来关闭返回值优化效果。
g++ -std=c++11 -fno-elide-constructors -o auto auto.cpp
输出结果:
从上面的例子中可以看到,在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是GetA()函数 内部创建的对象返回出来构造一个临时对象产生的,另一次是在main函数中构造a对象产生的。第二次的destruct 是因为临时对象在构造a对象之后就销毁了。如果开启返回值优化的话,输出结果将是:
construct: 1
destruct: 1
可以看到返回值优化将会把临时对象优化掉,但这不是c++标准,是各编译器的优化规则。 我们在回到之前提到的可以通过右值引用来延长临时右值的生命周期,如果上面的代码中我们通过右值引用来绑定 函数返回值的话,结果又会是什么样的呢?在编译时设置编译选项-fno-elide-constructors。
int main() {
A&& a = GetA();
return 0;
}
输出结果:
construct: 1
copy construct: 1
destruct: 1
destruct: 2
通过右值引用,比之前少了一次拷贝构造和一次析构,原因在于右值引用绑定了右值,让临时右值的生命周期延长了。我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构,事实上,在c++98/03中,通过常量左值引用也经常用来做性能优化。上面的代码改成:
const A& a = GetA();
输出的结果和右值引用一样,因为常量左值引用是一个“万能”的引用类型,可以接受左值、右值、常量左值和常量右值。需要注意的是普通的左值引用不能接受右值,比如这样的写法是不对的:
A& a = GetA();
上面的代码会报一个编译错误,因为非常量左值引用只能接受左值。
② 右值引用的第二个特点
右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值。比如下面的例子:
int&& var1 = 1;
var1类型为右值引用,但var1本身是左值,因为具名变量都是左值。
关于右值引用一个有意思的问题是:T&&是什么,一定是右值吗?让我们来看看下面的例子:
template<typename T>
void f(T&& t){}
f(10); //t是右值
int x = 10;
f(x); //t是左值
从上面的代码中可以看到,T&&表示的值类型不确定,可能是左值又可能是右值,这一点看起来有点奇怪,这就是右值引用的一个特点。
③ 右值引用的第三个特点
T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值; 如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。
我们再回过头看上面的代码,对于函数template
再看看下面的例子:
template<typename T>
void f(T&& param);
template<typename T>
class Test {
Test(Test&& rhs);
};
上面的例子中,param是universal reference,rhs是Test&&右值引用,因为模版函数f发生了类型推断,而Test&&并没有发生类型推导,因为Test&&是确定的类型了。
正是因为右值引用可能是左值也可能是右值,依赖于初始化,并不是一下子就确定的特点,我们可以利用这一点做很多文章, 比如后面要介绍的移动语义和完美转发。
这里再提一下引用折叠,正是因为引入了右值引用,所以可能存在左值引用与右值引用和右值引用与右值引用的折叠, C++11确定了引用折叠的规则,规则是这样的:
-
- 所有的右值引用叠加到右值引用上仍然还是一个右值引用;
-
- 所有的其他引用类型之间的叠加都将变成左值引用。
3, 第3行代码的故事
T(T&& a) : m_val(val){ a.m_val=nullptr; }
这行代码实际上来自于一个类的构造函数,构造函数的一个参数是一个右值引用,为什么将右值引用作为构造函数的参数呢?在解答这个问题之前我们先看一个例子
class A
{
public:
A():m_ptr(new int(0)){cout << "construct" << endl;}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数
{
cout << "copy construct" << endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
A GetA()
{
return A();
}
int main() {
A a = GetA();
return 0;
}
输出:g++编译的结构
construct
copy construct
copy construct
这个例子很简单,一个带有堆内存的类,必须提供一个深拷贝拷贝构造函数,因为默认的拷贝构造函数是浅拷贝,会发生“指针悬挂”的问题。 如果不提供深拷贝的拷贝构造函数,上面的测试代码将会发生错误(编译选项-fno-elide-constructors),内部的m_ptr将会被删除两次, 一次是临时右值析构的时候删除一次,第二次外面构造的a对象释放时删除一次,而这两个对象的m_ptr是同一个指针,这就是所谓的指针 悬挂问题。提供深拷贝的拷贝构造函数虽然可以保证正确,但是在有些时候会造成额外的性能损耗,因为有时候这种深拷贝是不必要的。 比如下面的代码:
上面代码中的GetA函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象a,临时变量在拷贝构造完成之后就销毁了, 如果堆内存很大的话,那么,这个拷贝构造的代价会很大,带来了额外的性能损失。每次都会产生临时变量并造成额外的性能损失, 有没有办法避免临时变量造成的性能损失呢?答案是肯定的,C++11已经有了解决方法,
看看下面的代码
class A
{
public:
A() :m_ptr(new int(0)){}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数
{
std::cout << "copy construct" << std::endl;
}
A(A&& a) :m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
std::cout << "move construct" << std::endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
A GetA()
{
return A();
}
int main(){
A a = GetA(); //1. g++ 转移函数 两次, 2. vs2015编译器 不调用转移函数 原因是编译会进行RVO优化
A a2 = A(GetA()); // 1. VS2015 调用一次 转移函数, 2. g++ 默认调用 三次
//-- 实际上面这段代码并不标准,依托编译器实现
//两个编译差异在C++11中提供std::move()转移函数
A a3 = std::move(a); // g++ , vs2015 都调用一次 A a(std::move(A())); A a1(std::move(Get()));
return 0;
}
输出:
construct
move construct
move construct
- g++ 转移函数 两次, 2. vs2015编译器 不调用转移函数 原因是编译会进行RVO优化
- A a2 = A(GetA()); // 1. VS2015 调用一次 转移函数, 2. g++ 默认调用 三次
- – 实际上面这段代码并不标准,依托编译器实现
- 两个编译差异在C++11中提供std::move()转移函数
- A a3 = std::move(a); // g++ , vs2015 都调用一次 A a(std::move(A())); A a1(std::move(Get()));
只多了一个构造函数,输出结果表明,并没有调用拷贝构造函数,只调用了move construct函数,
右值引用:
左值引用与右值引用大可参照左值与右值。我们知道可以使用std::move将一个左值转换为右值引用以便调用移动函数。那么问题来了,右值引用可以使用std::move吗?答案是肯定的。具体来讲,左值引用和右值引用是可以相互绑定的,但它们遵循规则:
非const左值引用只能绑定到非const左值; const左值引用可绑定到const左值、非const左值、const右值、非const右值; 非const右值引用只能绑定到非const右值,但不适用于函数模板的形参; const右值引用可绑定到const右值和非const右值,它没有现实意义(毕竟右值引用的初衷在于移动语义,而移动就意味着修改);
让我们来看看这个move construct函数:
A(A&& a) :m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
cout << "move construct" << endl;
}
这个构造函数并没有做深拷贝,仅仅是将指针的所有者转移到了另外一个对象,同时,将参数对象a的指针置为空,这里仅仅是做了浅拷贝,因此,这个构造函数避免了临时变量的深拷贝问题。
上面这个函数其实就是移动构造函数,他的参数是一个右值引用类型,这里的A&&表示右值,为什么?前面已经提到,这里没有发生类型推断,是确定的右值引用类型。为什么会匹配到这个构造函数?因为这个构造函数只能接受右值参数,而函数返回值是右值,所以就会匹配到这个构造函数。这里的A&&可以看作是临时值的标识,对于临时值我们仅仅需要做浅拷贝即可,无需再做深拷贝,从而解决了前面提到的临时变量拷贝构造产生的性能损失的问题。这就是所谓的移动语义,右值引用的一个重要作用是用来支持移动语义的。
需要注意的一个细节是,我们提供移动构造函数的同时也会提供一个拷贝构造函数,以防止移动不成功的时候还能拷贝构造,使我们的代码更安全。
我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借助移动语义来优化性能呢,那该怎么做呢?事实上C++11为了解决这个问题,提供了std::move方法来将左值转换为右值,从而方便应用移动语义。move是将对象资源的所有权从一个对象转移到另一个对象,只是转移,没有内存的拷贝,这就是所谓的move语义。
再看看下面的例子:
{
std::list< std::string> tokens;
//省略初始化...
std::list< std::string> t = tokens; //这里存在拷贝
}
std::list< std::string> tokens;
std::list< std::string> t = std::move(tokens); //这里没有拷贝
如果不用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。他实际上将左值变成右值引用,然后应用移动语义,调用移动构造函数,就避免了拷贝,提高了程序性能。如果一个对象内部有较大的对内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。事实上,C++11中所有的容器都实现了移动语义,方便我们做性能优化。
这里也要注意对move语义的误解,move实际上它并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用。如果是一些基本类型比如int和char[10]定长数组等类型,使用move的话仍然会发生拷贝(因为没有对应的移动构造函数)。所以,move对于含资源(堆内存或句柄)的对象来说更有意义。
4, 第4行代码故事
template <typename T>void f(T&& val){ foo(std::forward<T>(val)); }
C++11之前调用模板函数时,存在一个比较头疼的问题,如何正确的传递参数。比如:
template <typename T>
void forwardValue(T& val)
{
processValue(val); //右值参数会变成左值
}
template <typename T>
void forwardValue(const T& val)
{
processValue(val); //参数都变成常量左值引用了
}
都不能按照参数的本来的类型进行转发。
C++11引入了完美转发:在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征), 将参数传递给函数模板中调用的另外一个函数。C++11中的std::forward正是做这个事情的,他会按照参数的实际类型进行转发。
看下面的例子:
void processValue(int& a){ cout << "lvalue" << endl; }
void processValue(int&& a){ cout << "rvalue" << endl; }
template <typename T>
void forwardValue(T&& val)
{
processValue(std::forward<T>(val)); //照参数本来的类型进行转发。
}
void Testdelcl()
{
int i = 0;
forwardValue(i); //传入左值
forwardValue(0);//传入右值
}
输出:
lvaue
rvalue
右值引用T&&是一个universal references,可以接受左值或者右值,正是这个特性让他适合作为一个参数的路由, 然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发。
我们可以结合完美转发和移动语义来实现一个泛型的工厂函数,这个工厂函数可以创建所有类型的对象。
具体实现如下:
template<typename… Args>
T* Instance(Args&&… args)
{
return new T(std::forward<Args >(args)…);
}
这个工厂函数的参数是右值引用类型,内部使用std::forward按照参数的实际类型进行转发,如果参数的实际类型是右值, 那么创建的时候会自动匹配移动构造,如果是左值则会匹配拷贝构造。
通过4行代码我们知道了什么是右值和右值引用,以及右值引用的一些特点,利用这些特点我们才方便实现移动语义和完美转发。C++11正是通过引入右值引用来优化性能,具体来说是通过移动语义来避免无谓拷贝的问题,通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去,通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)。
二, RAII机制
参考: 博客地址:https://blog.csdn.net/10km/article/details/49847271
RAII的例子:
lock_guard
C++11中lock_guard对mutex互斥锁的管理就是典型的RAII机制,以下是C++11头文件
/// @brief Scoped lock idiom.
// Acquire the mutex here with a constructor call, then release with
// the destructor call in accordance with RAII style.
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对象的拷贝构造函数和赋值运算符,以确保lock_guard不会被复制,这是RAII机制的一个基本特征,后面所有RAII实现都具备这个特性。
① 对自定义锁的RAII实现
它实现了对资源的共享读取和独占写入。基本的接口如下:
class RWLock {
int readLock();
int readUnlock();
int writeLock();
int writeUnlock();
};
如果我要用RAII方式管理RWLock对象,我就要写一个针对RWLock的类,因为RWLock分为读取锁和写入锁两种加锁方式,所以不能使用上节中现成的std::lock_guard来实现RAII管理, 那么,我就要分别针对两种类型写两个不同的类。下面是管理读取锁的RAII类代码:
class RWLockGuard_R{
public:
RWLockGuard_R(RWLock & lock):lock(lock){lock.readLock();}
~RWLockGuard_R(){lock.readUnlock();}
RWLockGuard_R(const RWLockGuard_R&) = delete;
RWLockGuard_R& operator=(const RWLockGuard_R&) = delete;
private:
RWLock & lock;
};
当然,关于管理RWLock写入锁的RAII类写法也大同小异,无非是把readUnlock和readLock替换为writeLock和writeUnlock
调用方式也与前面的lock_guard相似
RWLock lock;
void raii_test(){
RWLockGuard_R guard(lock);
//do something...
}
int main(){
raii_test();
}
② 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()>;
/* release: 析构时执行的函数
* acquire: 构造函数执行的函数
* default_com:_commit,默认值,可以通过commit()函数重新设置
*/
explicit raii(fun_type release, fun_type acquire = [] {}, bool default_com = true) noexcept:
_commit(default_com), _release(release) {
acquire();
}
/* 对象析构时根据_commit标志执行_release函数 */
~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管理类
* T为资源类型
* acquire为申请资源动作,返回资源T
* release为释放资源动作,释放资源T
*/
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
}
/* 对象析构时根据_commit标志执行_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类,
// 有点类似于java的反射(reflect)提供的功能,不过只能用于编译期,不能用于运行时。
// 关于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*/
上面的代码中其实包括了两个类(raii,raii_var)和两个函数(make_raii参数重载),对应着代码提供的三种实现通用RAII机制的方式:
raii是基于可调用对象(Callable Object)来实现的通用RAII机制,直接以可调用对象定义申请资源和释放资源的动作为类初始化参数构造raii对象。适合任何类型(包括非对象资源)资源的RAII管理。 raii_var是实现对于实体资源(非互斥锁)的通用RAII机制模板类。适合实体类(包括非对象资源)资源的RAII管理。 模板函数make_raii在raii基础上做了进一步封装,对于资源对象(struct/class)指定资源对象成员函数分别作为申请资源和释放资源的动作。适用于class类型的资源对象(struct/class) 下面分别对两种方式的用法举例:
raii 还以前面的RWLock资源锁为例:
RWLock lock;
void raii_test(){
raii guard_r([&](){lock.readUnlock();},[&](){lock.readLock();},true);
//do something...
}
int main(){
raii_test();
}
上面的例子中直接用lambda表达分别构造了释放/申请资源的可调用对象(Callable Object)做参数
再举一个文件操作的例子:
#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可以这样写:
#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
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...
}
int main(){
make_raii_test();
}
如果只有释放资源(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...
}
int main(){
make_raii_test();
}
到这里,本文就算结束了,文中主要提出了一种模板化的RAII机制实现方法,给出了三种RAII使用方法,你可以根据自己需要选择两种方法中的任意一种。 显然第一种直接构造raii对象的方法更通用,适合于任何类型资源, 第二种raii_var模板类适用于实体类资源比如打开关闭文件这种acquire动作有返回资源对象的, 第三种使用make_raii模板函数构造raii对象的方法对于对象型资源(struct/class)更方便。 如果是自己项目中的资源类对象,再对make_raii进一步封装,使用起来就更方便了。
三, RAII应用 异常安全
在有异常的情况下要写出正确的代码不是一件容易的事,让我们一起来面对它。异常建立了一个单独的控制流,它和应用程序的主控制流几乎没有关系。要了解异常的控制流需要一种不同的思维方式,并且需要新的工具。
1, 写异常安全的代码是困难的:一个例子
比如说你正在开发一个现在时髦的即时消息服务器程序。用户可以登录和注销,并且可以互相发送消息。你有一个服务器端的数据库保存用户信息,并且在内存里记录已登录的用户。每个用户可以有好友列表,这个列表同时在内存里和数据库里保存。
当一个用户增加或者删除一个好友时,你需要做两件事:更新数据库,并且更新内存中那个用户的缓存。就这么简单的一件事。
假设你的模型里每个用户的信息用一个叫 User 的类来表示,用户数据库用 UserDatabase 类表示。增加一个好友的操作看起来就象下面这样:
class User
{
// ...
string GetName();
void AddFriend(User& newFriend);
private:
typedef vector<User*> UserCont;
UserCont friends_;
UserDatabase* pDB_;
};
void User::AddFriend(User& newFriend)
{
// Add the new friend to the database
pDB_->AddFriend(GetName(), newFriend.GetName());
// Add the new friend to the vector of friends
friends_.push_back(&newFriend);
}
令人吃惊的是,只有两行的User::AddFriend里隐藏了一个致命的错误。在内存用尽的情况下,vector::push_back会通过抛出异常来表示操作失败。在那种情况下,你最终只把好友加到数据库里去,但是没有加到内存信息里。
现在我们遇到了问题,是吗?在任何情况下,缺少信息一致性是危险的。很可能在你的应用程序里的很多地方都假设数据库里的信息和内存里的是同步的。
一个简单的解决方法是考虑交换两行代码的顺序:
void User::AddFriend(User& newFriend)
{
// Add the new friend to the vector of friends
// If this throws, the friend is not added to
// the vector, nor the database
friends_.push_back(&newFriend);
// Add the new friend to the database
pDB_->AddFriend(GetName(), newFriend.GetName());
}
这确实能在vector::push_back失败的情况下保护数据一致性。不幸的是,当你查看UserDatabase::AddFriend的文档,你发现它也会抛出异常。现在你会把好友加到vector里,但没有加到数据库里。
这时候你会质问做数据库的人们:“为什么你们不返回出错代码,而要抛出异常呢?”他们会说:“我们使用的是一个运行在TZN网络上的高可靠性的集群数据库服务器,极少出错。因此,我们认为应该用异常来表示出错是最好的,因为异常只出现在异常的情况下,不是吗?”
这个理由讲得通,但是你还是要处理出错情况。你不会希望因为数据库出错而导致整个系统一片混乱。这样你修复数据库时,不必关闭整个服务器程序。
本质上,你必须做两个操作,它们中的任何一个都可能失败。当其中一个失败时,你必须撤销全部操作。让我们来看看怎么做这件事。
2, 用ScopeGuard——我们稍后详细介绍——你很容易就可以写出简洁、正确而高效的代码:
void User::AddFriend(User& newFriend)
{
friends_.push_back(&newFriend);
ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);
pDB_->AddFriend(GetName(), newFriend.GetName());
guard.Dismiss();
}
上面代码里,guard对象唯一的任务就是在它离开作用域时,调用friends_.pop_back,除非你调用了Dismiss。如果你调用了,那么guard就什么也不做
ScopeGuard在它的析构函数里实现自动调用某个全局函数或者成员函数。在有异常的情况下,你会想要实现自动撤销原子操作的功能,这时候ScopeGuard会很有用。
你可以这样使用ScopeGuard:如果你希望几个操作按照“要么全做,要么全不做”的方式工作,你可以在紧接着每个操作后面放一个ScopeGuard,这个ScopeGuard可以取消前面的操作:
friends_.push_back(&newFriend);
ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);
ScopeGuard也可用于普通函数:
void* buffer = std::malloc(1024);
ScopeGuard freeIt = MakeGuard(std::free, buffer);
FILE* topSecret = std::fopen("cia.txt");
ScopeGuard closeIt = MakeGuard(std::fclose, topSecret);
当整个原子操作成功时,你Dismiss所有guard对象。否则每个ScopeGuard对象会忠实的调用你构造它时所传的那个函数。
有了ScopeGuard,你可以简单的安置各种撤销操作,而不再需要写特别的类来做诸如删除vector的最后一个元素、释放内存、关闭文件这样的事情。这使ScopeGuard成为编写异常安全代码的一个极其有用、并且可重用的解决方案,它使一切变得很简单。
3, 实现ScopeGuard
ScopeGuard是对C++惯用法RAII(资源分配即初始化)典型实现的一个推广。它们的区别在于ScopeGuard只关注资源清理的那部分—— 资源分配由你自己做,而ScopeGuard处理资源的释放(事实上,可以论证清理工作是这个谚语里最重要的部分)。
释放资源有很多种形式,比如调用一个函数、调用一个functor、或者调用一个对象的成员函数,而每种方式都可能有零个、一个或者更多的参数。
自然,我们通过一个类层次关系来对这些变体建模。层次中各个类的对象的析构函数完成实际工作。层次中的根为ScopeGuardImplBase类,如下:
class ScopeGuardImplBase
{
public:
void Dismiss() const throw()
{ dismissed_ = true; }
protected:
ScopeGuardImplBase() : dismissed_(false)
{}
ScopeGuardImplBase(const ScopeGuardImplBase& other)
: dismissed_(other.dismissed_)
{ other.Dismiss(); }
~ScopeGuardImplBase() {} // nonvirtual (see below why)
mutable bool dismissed_;
private:
// Disable assignment
ScopeGuardImplBase& operator=(const ScopeGuardImplBase&);
};
ScopeGuardImplBase集中了对dismissed_标志的管理,这个标志控制派生类是否要执行清理工作。如果dismissed_为真,则派生类在他们的析构函数里什么也不做。
现在我们来看看ScopeGuardImplBase析构函数定义里缺少的virtual。如果析构函数不是virtual的,你怎么可以期望析构函数有正确的多态行为呢?好,把你的好奇心再保持一会儿,我们手里还有张王牌,我们可以通过它得到多态的析构行为,而不必付出虚函数的代价。
现在我们先来看看怎么实现这样一个对象,它在析构函数里调用一个带一个参数的函数或者functor。然而当你调用了Dismiss,那么这个函数或者functor就不会被调用。
template <typename Fun, typename Parm>
class ScopeGuardImpl1 : public ScopeGuardImplBase
{
public:
ScopeGuardImpl1(const Fun& fun, const Parm& parm)
: fun_(fun), parm_(parm)
{}
~ScopeGuardImpl1()
{
if (!dismissed_) fun_(parm_);
}
private:
Fun fun_;
const Parm parm_;
};
为了方便使用ScopeGuardImpl1,我们写一个辅助函数。
template <typename Fun, typename Parm>
ScopeGuardImpl1<Fun, Parm>
MakeGuard(const Fun& fun, const Parm& parm)
{
return ScopeGuardImpl1<Fun, Parm>(fun, parm);
}
MakeGuard依靠编译器推导出模板函数中的模板参数,这样你就不用自己指定ScopeGuardImpl1的模板参数了——事实上你不需要显式创建 ScopeGuardImpl1对象。这个技巧也被一些标准库中的函数所使用,如make_pair和bind1st。
你还对不使用虚构造函数而得到多态性析构行为的方法感到好奇吗?下面是ScopeGuard的定义,会让你大吃一惊的是,它仅仅是一个typedef:
typedef const ScopeGuardImplBase& ScopeGuard;
好了现在让我们来揭开全部神秘机制。根据C++标准,如果const的引用被初始化为对一个临时变量的引用,那么它会使这个临时变量的生命期变得和它自己一样。让我们举个例子来解释这件事。如果你写:
FILE* topSecret = std::fopen("cia.txt");
ScopeGuard closeIt = MakeGuard(std::fclose, topSecret);
那么MakeGuard创建了一个临时变量,它的类型为(看以前做一下深呼吸):
ScopeGuardImpl1<int (&)(FILE*), FILE*>
这是因为std::fclose是接受FILE*类型参数返回int的函数。具有上面那个类型的临时变量被指派给了const引用closeIt。根据上面提到的C++语言规则,这个临时变量会和它的引用closeIt有同样长的生存期——当这个临时变量被析构时,会调用正确的析构函数。接着,析构函数关闭文件。
ScopeGuardImpl1支持有带参数的函数(或functor)。很容易就可以写出不带参数、带两个参数或带更多参数的类(ScopeGuardImpl0、ScopeGuardImpl2??)。当你有了这些类,你就可以重载MakeGuard,从而得到一个优美、统一的语法:
template <typename Fun>
ScopeGuardImpl0<Fun>
MakeGuard(const Fun& fun)
{
return ScopeGuardImpl0<Fun >(fun);
}
到现在为止,我们已经有了一个强大的工具来表达调用一组函数的原子操作。MakeGuard是一个优秀的工具,特别是它同样可以用于C语言的API,而不需要写很多包装类。
更好的是,它不损失效率,因为它不涉及到虚函数调用。
- 针对对象和成员函数的ScopeGuard
到现在为止,一切都很好,但是怎么调用对象的成员函数呢?其实这一点也不难。让我们来实现ObjScopeGuardImpl0,一个可以调用对象的无参数成员函数的类模板。
template <class Obj, typename MemFun>
class ObjScopeGuardImpl0 : public ScopeGuardImplBase
{
public:
ObjScopeGuardImpl0(Obj& obj, MemFun memFun)
: obj_(obj), memFun_(memFun)
{}
~ObjScopeGuardImpl0()
{
if (!dismissed_) (obj_.*fun_)();
}
private:
Obj& obj_;
MemFun memFun_;
};
ObjScopeGuardImpl0有一点特别,因为它用了不太为人所知的语法:指向成员函数的指针和operator.*()。为了理解它是如何工作的,让我们来看看MakeObjGuard的实现(我们在本节开始已经利用过MakeObjGuard了)。
template <class Obj, typename MemFun>
ObjScopeGuardImpl0<Obj, MemFun, Parm>
MakeObjGuard(Obj& obj, Fun fun)
{
return ObjScopeGuardImpl0<Obj, MemFun>(obj, fun);
}
现在,如果你调用:
ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);
会创建一个如下类型的对象:
ObjScopeGuardImpl0<UserCont, void (UserCont::*)()>
幸好,MakeObjGuard让你免于写那些跟字符型图标一样单调的类型。工作机制还是一样——当guard离开作用域,临时对象的析构函数会被调用。析构函数通过指向成员的指针调用成员函数。这里我们用到了.*操作符。
4, 错误处理
如果你读过Herb Sutter关于异常的著作[2],你就会知道一条基本原则:析构函数不应该抛出异常。一个会抛出异常的析构函数会让你无法写出正确的代码,并且会再没有任何警告的情况下终止你的应用程序。在C++里,当一个异常被抛出,在堆栈展开(unwinding)的时候某个析构函数又抛出另一个异常,应用程序会被马上终止。
ScopeGuardImplX和ObjScopeGuardImplX分别调用了一个未知的函数或成员函数,那个函数可能会抛出异常。这会终止程序,因为我们设计guard的析构函数的目的就是:当有异常被抛出,在展开(unwinding)堆栈时,调用这个未知函数!理论上,你不应该把可能抛出异常的函数传给MakeGuard或者MakeObjGuard。在实用中(你可以从供下载的代码中看到),析构函数对异常采取了防御措施。
template <class Obj, typename MemFun>
class ObjScopeGuardImpl0 : public ScopeGuardImplBase
{
...
public:
~ScopeGuardImpl1()
{
if (!dismissed_)
try { (obj_.*fun_)(); }
catch(...) {}
}
}
是的,catch(…)块什么事也不做。这可不是随手写的,这在异常处理的领域中是很基本的:如果你的“撤销/恢复”操作也失败了,那么你几乎没有什么事情可以做了。你尝试恢复,但是不管恢复操作是否成功,你都应该继续下去。
以我们的即时消息为例,一个可能动作顺序是:你向数据库里加入了一个好友数据,但当把它插入friends_ vector时失败了,当然你会尝试把它从数据库里再删掉。虽然可能性很小,但是从数据库里删除数据时,不知道为什么也失败了,这种情况就很讨厌了。
一般来说,你应该在那些保证可以成功撤销的操作上使用guard
5, 支持传引用的参数
在Petru和我很高兴地使用ScopeGuard一段时间以后,我们遇到一个问题。考虑下面的代码:
void Decrement(int& x) { --x; }
void UseResource(int refCount)
{
++refCount;
ScopeGuard guard = MakeGuard(Decrement, refCount);
}
上面代码中的guard对象确保refCount的值在UseResource函数退出时保持不变。(这个惯用法在一些共享资源的情况下很有用。)
尽管有用,但上面的代码不能工作。问题在于,ScopeGuard保存了refCount的一个拷贝(看一下ScopeGuardImpl1的定义,在成员变量parm_里)而不是对它的引用。然而在这个例子里,我们需要的是保存refCount的一个引用,这样才能让Decrement对它进行操作。
一个解决办法是再实现一些类,例如ScopeGuardImplRef,以及MakeGuardRef。这会有很多重复劳动,并且在实现处理多参数的类时,这个办法就很难应付了。
我们采取的办法是:使用一个辅助类,它把引用转变为一个值。
template <class T>
class RefHolder
{
T& ref_;
public:
RefHolder(T& ref) : ref_(ref) {}
operator T& () const
{
return ref_;
}
};
template <class T>
inline RefHolder<T> ByRef(T& t)
{
return RefHolder<T>(t);
}
RefHolder以及和它配套的辅助函数ByRef可以无缝地使引用适合于值的语义,并且使ScopeGuardImpl1不需要任何改变就可以使用引用。你要做的只是把引用形式的参数用ByRef包装一下,就象这样:
void Decrement(int& x) { --x; }
void UseResource(int refCount)
{
++refCount;
ScopeGuard guard = MakeGuard(Decrement, ByRef(refCount));
...
}
我们发现这个方法很有说明性,它提醒你正在用引用方式传递参数。
这个支持引用的办法最好的一点是在ScopeGuardImpl1中的const修饰。这里是相关的代码摘要:
template <typename Fun, typename Parm>
class ScopeGuardImpl1 : public ScopeGuardImplBase
{
...
private:
Fun fun_;
const Parm parm_;
};
这个小小的const非常重要。它防止使用非const引用的代码通过编译和不正确地运行。换句话说,如果你忘记使用ByRef,编译器不会让这样的错误代码通过。
看一下loki库中实现
结语
我们讨论了一些在编写异常安全代码中出现的一些情况。在比较了几个在这些情况下获得异常安全性的方法以后,我们介绍了一个方法,适用于有防错性(并且不会再throw)撤销操作可用的情况。ScopeGuard使用了若干泛型编程的技术,让你指定在ScopeGuard退出代码块时调用的函数和成员函数。作为可选项,你也可以解除ScopeGuard对象的动作。
当你需要实行资源的自动释放,并且可以依靠防错的撤销操作,ScopeGuard在着这种情况下对你很有帮助。当你把几个可能会失败,但是也可以撤销的操作组成一个原子操作,这个惯用法就变得很重要了。当然这个方法也有不适用的情况。