首页 > 代码库 > RAII和unique_ptr
RAII和unique_ptr
RAII
RAII是Resource Acquisition Is Initialization的缩写,是在面向对象(object-oriented)语言中使用的一种编程习惯,主要是用来在C++中处理异常安全资源管理(exception-safe resource management)。
在RAII中,资源的获取和释放和对象的声明周期紧密联系在一起,当对象构造的时候,在构造函数中申请资源( resource allocation),而在对象的析构函数中释放资源(resource deallocation),当析构函数正确执行时,就不会有资源泄露,这让我想起了Linux内核驱动,在编写驱动的过程中,一般也是在init函数中申请资源,然后在exit函数中释放资源,能够避免资源泄露,因此这种思想普遍运用在编程中。
优点
RAII作为一种资源管理的技术主要有以下优点:
- 封装性,因为将异常处理的逻辑写到了构造和析构函数中,因此不用调用者关心怎么对资源进行管理。
- 异常安装,对于栈对象,当异常发生的时候,异常处理会在离开当前scope的时候,执行对象的析构函数,从而释放内存。
- 可定位,能够将资源申请和释放的逻辑集中写在构造和析构中,不会散乱在各个地方。
典型应用
RAII作为一种资源管理技术,主要运用于下面几个地方: 1. 在多线程环境下控制同步锁。在多线程中,为了线程间同步,经常要申请、释放锁,而有时候,经常会出现申请了,但是没有或者由于异常等原因没有释放锁,从而出现死锁。对于这种申请、释放锁的共走,可以交给对象的构造和析构函数,这样可以将申请、释放封装起来,并且保证了异常安全。 2. 文件处理,一般在处理文件的时候,我们都要先open-write/read-close,对于这种固定的结构,完成可以将read-write封装起来。 3. 动态对象的所有权问题。针对动态对象的所有权,C++提供了smart pointer
,其中std::unique_ptr
针对单拥有权问题,而std::shared_ptr
则是针对共享对象。
例子:
#include <string>
#include <mutex>
#include <iostream>
#include <fstream>
#include <stdexcept>
void write_to_file (const std::string & message) {
// 互斥访问
static std::mutex mutex;
// 加锁
std::lock_guard<std::mutex> lock(mutex);
// 打开文件
std::ofstream file("example.txt");
if (!file.is_open())
throw std::runtime_error("unable to open file");
// 写
file << message << std::endl;
// 在离开scope的时候,ofstream在析构函数中关闭文件,
// lock_guard会在构造中加锁,在析构中解锁
}
GCC对于C的扩展
gnu针对C提供了一种非标准的扩展来支持RAII: "cleanup" variable attribute。下面是一个例子:
static inline void fclosep(FILE **fp) { if (*fp) fclose(*fp); }
#define _cleanup_fclose_ __attribute__((cleanup(fclosep)))
void example_usage() {
_cleanup_fclose_ FILE *logfile = fopen("logfile.txt", "w+");
fputs("hello logfile!", logfile);
}
下面对前面提到的unique_ptr进行介绍。
C++11: unique_ptr
unique_ptr是C++11新增的一个特性,作为一种新的smart pointer,主要用于管理只有单一拥有权的动态对象。
基本用法:
std::unique_ptr<foo> p( new foo(42) );
unique_ptr有着智能指针的优点,在析构函数中会自动释放资源。
在C++11之前有auto_ptr用来做动态对象的管理,但是复制auto_ptr会将控制权从右值转移到左值,原先的右值将不再拥有对象的控制权,这和传统的“复制”语义不符,而且由于这种复制语义,导致了auto_ptr不能被用在标准的容器中。在C++11中出现了unique_ptr开解决这个问题。使用unique_ptr的时候,unique_ptr可以存储在容器中,当容器析构的时候unique_ptr指向的对象也被释放。
那C++11是怎么解决不能存在在容器中的问题的呢?通过添加了rvalue reference和move语义。
什么是rvalue reference?
rvalue reference是C++的一个小扩展,rvalue reference允许coder避免逻辑上不必要的复制,并且提供了一个完美的转发功能。主要目的是帮助设计高性能、健壮的库。在A Brief Introduction to Rvalue References中有对rvalue reference的介绍。那为什么要引入rvalue reference,引入rvalue reference解决了什么问题呢?
Lvalues vs rvalues
传递给函数参数的时候,我们可以传值或者传引用。在C中,传引用是通过传递指针,而指针实际上就是传递的一个地址,在C/C++中,内存模型就是申请一个box,然后在这个box中存放数据,而传递的时候就是传递这个box的地址,对于这个box怎么管理从而引申出好多问题。但是不是所有的数据都有地址,譬如某些存放在寄存器中的数据,此时就没有地址了,因此,对于有地址的数据我们叫做lvalues
,而没地址的数据叫rvalues
。那实际中有哪些rvalues
,譬如函数的返回值,数值计算式等。
下面是一个例子:
void f(int *pi);
int g();
f(&g()); //error
f(&(1+1)); // error
上面这个是无法通过的,因为传入的参数是右值,没有地址,一个简单的修改如下:
int temp1 = g();
f(&temp1);
int temp2 = 1 + 1;
f(&temp2;
那为什么编译器不帮我们创建临时变量,然后不再报错呢?
让我们想下,一般当参数是地址的时候,我们在函数内部是希望改变指向的对象的值,如:
void f(int *pi){ ++*pi; }
此时我们如果传入一个临时变量,对于这个函数内部的操作就变的没有意义了。因此,此时编译器就会报错,不允许将rvalue的地址传入。
Reference
在C++中,除了指针外,还加入了引用,于是,上面的代码可以重写如下:
void f(int &pi){ pi++; }
int g();
f(g()); //error
f((1+1)); // error
此时还是无法得到rvalue的引用(reference),但是有时候传递引用并不是为了改变其值,对于一些大的数据,传递引用可以减少开销,此时并不对lvalue和rvalue有要求。因此,编译器因该能够允许传递rvalue的reference。
Const reference
当我们不会改变传递进来的reference的值时,则传递进来lvalue或者rvalue都是可以的,因此,上面的代码如果改成下面的:
void f(const int &pi){ pi++; }
int g();
f(g()); //ok
f((1+1)); // ok
此时看起来一切都挺好的,但是引入ato_ptr后,就会带来一系列的问题。
现在总结下reference和lvalue和rvalue的关系:
- reference可以绑定到lvalues
- const reference可以绑定到lvalues和rvalues,但是不允许改变源值
上面看上去都挺好的,除了不能将reference绑定到rvalues,并且修改,但是谁又想要改变一个临时的值呢?
auto_ptr
auto_ptr是智能指针中的一员,用来自动释放指向的内存块,auto_ptr有value的语义,可以传递,在栈上创建,或者是其他数据的长成员,在传递给函数的时候是通过值传递,但是当auto_ptr进行值传递的时候,其指向的数据并不复制,这又像reference的行为,同时可以对auto_ptr使用*和->,就像指针一样。
考虑下面的代码:
auto_ptr<int> create() {
return auto_ptr<int>(new int(42));
}
auto_ptr<int> ap = create();
考虑上面的代码,在create()内部,当结束的时候,刚离开scope的时候,如果不做什么,编译器会调用auto_ptr的析构函数,从而释放内存;函数create返回的是一个rvalue。
对于第一点,调用析构函数,我们希望将其赋给ap,此时因该调用拷贝构造函数,而对于右值有下面的两个问题:
- 如果我们定义拷贝构造函数的source为const reference,则此时无法修改source,将拥有权转移。
- 如果定义非const的reference,此时不能够绑定到右值。
rvalue reference
对于上面的问题,我们需要的是rvalue reference,能够绑定到rvalue,并且修改他。于是针对auto_ptr不能复制构造右值,出现了unique_ptr
,unique_ptr
有下面的复制构造函数:
unique_ptr::unique_ptr(unique_ptr && src)
rvalue reference能够同时绑定到lvalue和rvalue,并且不阻止改变值。
why auto_ptr can‘t store auto_ptr objects in most containers
auto_ptr还有一点问题是,不能够存储在大多数容器中,为什么呢?考虑下面的一个例子:
auto_ptr<Foo> pSrc(new Foo);
auto_ptr<Foo> pDest = pSrc; // 看起来像像拷贝,但是不具备拷贝的语义,会将源中指针置空 pSrc->method(); // 运行时错误,因为此时pSrc中指针为空
而在容器中,会有临时变量,一旦拷贝,则容器中的值将不再有所有权,这是严重的错误。
但是有时候我们又想要将控制权进行转移,此时我们应该明确的告知说我们现在要转移控制权了,因此不要再去使用原先的源了。
此处我们总结下rvalue和lvalue的不同,rvalue会在赋值后消失,而lvalue则保持不变。我们看到上面的unique_ptr的复制构造函数:
unique_ptr::unique_ptr(unique_ptr && src)
这会同时绑定rvalue好lvalue,因此我们需要下面的一个重载函数:
unique_ptr::unique_ptr(unique_ptr & src)
重载rvalue,此时我们要做的就是将rvalue的构造私有化,现在unique_ptr只保持转移控制权的语义了,而当我们想要将一个lvalue转移到unique_ptr的时候,此时使用move函数,
unique_ptr pSrc(new Foo);
unique_ptr pDest = move(pSrc);
明确的将lvalue变为rvalue,move作用就是将一个lvalue转成rvalue,
template <class T>
typename remove_reference<T>::type&& move(T&& t) {
return t;
}
下面我们接着介绍unique_ptr,unique_ptr中unique在此处是指什么呢?此处unique表示,当你创建一个对象的时候,只会有一份拷贝,只会有一个指针。
平时我们使用指针的时候,经常会碰到下面的情形:
foo *p = new foo("useful object");
make_use( p );
首先创建一个对象,然后将指针传递给make_use,此时在make_use会对指针做什么呢?make_use会拷贝指针,稍后使用嘛?或者make_use会释放指针吗?我们无法很好的回答这些问题,因为C++不能保证make_use对指针的使用规范,我们只能通过检查代码来保证不会错误的时候指针。这些问题可以通过unique_ptr
解决,只保证一份拷贝,不会有其他拷贝。
现在我们使用指针的时候,将其放入到unique_ptr,保证拥有权是唯一的,不会隐式转移,要转移时通过明确的move函数将其转成rvalue,进行转移,此处,使用unique_ptr后,会带来的一点不同是,我们将会传递引用,将其当做值来传递,如下面:
void inc_baz( std::unique_ptr<foo> &p )
{
p->baz++;
}
总结
本文首先介绍了RAII,即Resource Acquisition Is Initialization一种资源安全管理的方式,然后引出了smart point,随后又介绍了auto_ptr,早期对RAII的一种尝试,后来由于其存在的一些问题,通过介绍lvalue、rvalue的概念,最后在C++11中给出了解决方案,unique_ptr,并给出move语义,明确的将左值转换为rvalue,进行控制权的转移。
参考:
http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization
http://www.drdobbs.com/cpp/c11-uniqueptr/240002708
http://bartoszmilewski.com/2008/10/18/who-ordered-rvalue-references-part-1/
<style></style>