RAII 与Pimpl 源地址http://tech.uc.cn/?p=851
RAII
RAII是Bjarne Stroustrup教授用于解决资源分配而发明的技术,资源获取即初始化。
RAII是C++的构造机制的直接使用,即利用构造函数分配资源,利用析构函数来回收资源。
我们知道,在C/C++语言中,对动态分配的内存的处理必须十分谨慎。在没有RAII应用的情况下,如果在内存释放之前就离开指针的作用域,这时候几乎没机会去释放该内存,除非垃圾回收器对其管制,否则我们要面对的将会是内存泄漏。
举个例子来说明下RAII在内存分配方面的使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
structByteArray{
unsignedchar*data_;
intlength_;
};
voidcreate_bytearray(ByteArray*,intlength);
voiddestroy_bytearray(ByteArray*);
voidbar(){
ByteArray ba;
create_bytearray(&ba,2048);
/* 使用 */
/* 如果有异常,Oops */
...
destroy_bytearray(&ba);
}
|
这是典型的C风格代码,没有应用RAII。
因此值得注意的是,destroy_bytearray必须在退出作用域前被调用。
然而在复杂的逻辑设计中,程序员往往要花大量的精力以确认所有在该作用域分配的ByteArray得到正确的释放。
相形之下,C++运行机制保证了栈上对象一旦即将离开作用域,其析构函数将被执行,给予了释放资源的时间。注意,在堆分配的对象必须调用delete来结束其生命。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
structByteArray{
ByteArray():length_(0),data_(0){}
ByteArray(intlength)
:length_(length){
data_=newunsignedchar[length];//< 注意这里或许会抛异常
memset(data_,0,length_);
}
~ByteArray(){
if(nullptr!=data_)delete data_;
}
unsignedchar*data_;
intlength_;
private:
ByteArray(constByteArray&);
};
voidbar(){
ByteArray ba(2048);
/* 使用 */
...
}//< 正确地被析构,没有内存泄漏
|
C++11 STL中的std::unique_ptr可用于控制作用域中的动态分配的对象。
譬如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include
voidbar(){
ByteArray*ba=newByteArray(2048);
std::unique_ptr holder(ba);
/* 使用 */
...
}//< 正确地被析构,没有内存泄漏
voidfoo(){
try{
bar();
}
catch(constchar*e){
...
}
catch(...){
...
}
}
|
函数bar()只是增加了一行,但强壮了很多,函数bar()执行完或者有异常抛出时,holder总会被析构,从而ba或被delete。
下面是ByteArray的Ada实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
-- lib.ads
withinterfaces;
withAda.Finalization;
packagelib is
typeuchars isarray(positive range<>)ofinterfaces.unsigned_8;
typeuchars_p isaccessuchars;
typeByteArray isnewAda.Finalization.Limited_Controlled withprivate;
functionCreate(length:integer)returnByteArray;
private
typeByteArray isnewAda.Finalization.Limited_Controlled withrecord
length:integer;
data:uchars_p;
endrecord;
overriding
procedureInitialize(This:inoutByteArray);
overriding
procedureFinalize(This:inoutByteArray);
endlib;
-- lib.adb
withAda.Unchecked_Deallocation;
packagebodylib is
useAda.Finalization;
functionCreate(length:integer)returnByteArray is
begin
iflength<0then
put_line("Create");
returnByteArray'(Limited_Controlled with length => length,
data=> new uchars(1..length));
end if;
return ByteArray'(Limited_Controlled withlength=>0,data=>null);
endCreate;
overriding
procedureInitialize(This:inoutByteArray)is
begin
put_line("Initialize");
this.length:=0;
this.data:=null;
endInitialize;
overriding
procedureFinalize(This:inoutByteArray)is
procedurefree isnewAda.Unchecked_Deallocation(uchars,uchars_p);
begin
put_line("Finalize");
if(this.data/=null)then
free(this.data);
endif;
endFinalize;
endlib;
-- main.adb
withlib;
uselib;
proceduremain is
K:ByteArray:=Create(10240);
C:ByteArray;
begin
null;
endmain;
|
– 输出如下
./main
Create
Initialize
Finalize
Finalize
另一种情况是对I/O资源的处理,当我们不再使用资源时,必须将资源归还给系统。
下面例子来自 wikipedia的RAII条目:
1
2
3
4
5
6
7
8
|
voidwrite_to_file(conststd::string&message){
staticstd::mutex mutex;
std::lock_guard lock(mutex);
std::ofstreamfile("example.txt");
if(!file.is_open())
throwstd::runtime_error("unable to open file");
file<<message<<std::endl;
}
|
在write_to_file函数中,RAII作用于std::ofstream和std::lock_guard,从而保证了函数write_to_file在返回时,lock和file总会调用自身的析构函数,对于lock而言,它会释放mutex,而file则会close。
Pimpl
Pimpl(pointer to implementation),是一种应用十分广泛的技术,它的别名也很多,如Opaque pointer, handle classes等。
wikipedia上已经对其就Ada、C和C++举例,这里不作举例。
个人认为,Pimpl是RAII的延展,籍由RAII对资源的控制,把具体的数据布局和实现从调用者视线内移开,从而简化了API接口,也使得ABI兼容变得有可能,Qt和KDE正是使用Pimpl来维护ABI的一致性,另外也为惰性初始化提供途径,以及隐式共享提供了基础。
我在设计代码时也会考虑使用Pimpl,但不是必然使用,因为Pimpl也会带来副作用,主要有两方面
- Pimpl指针导致内存空间开销增大
- 类型间Pimpl的访问需要较多间接的指针跳转,甚至还用使用
friend''来提升访问权限,如以下代码中,Teacher可以访问Student的Context。
1234567891011121314151617181920212223242526// student.hclassStudent{public:explicitStudent(constchar*name,intage);~Student();private:///< PimplstructContext;Context*constcontext_;friendclassTeacher;};// student_p.h#include "student.h"structStudent::Context{explicitContext(constchar*name,intage){...}//< 实质的数据存储在这里};// student.cpp#include "student_p.h"Student::Student(constchar*name,intage):context_(newContext(name,age){}...
尽管如此,我个人还是在面向开发应用的接口中会尽量使用Pimpl来维护API和ABI的一致性,除非Pimpl会引起显著的性能下降。