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
|
struct
ByteArray
{
unsigned
char*
data_;
int
length_;
};
void
create_bytearray(ByteArray*,
int
length);
void
destroy_bytearray(ByteArray*);
void
bar()
{
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
|
struct
ByteArray
{
ByteArray():length_(0),
data_(0)
{}
ByteArray(int
length)
:
length_(length)
{
data_
=
new
unsigned
char
[length];
//<
注意这里或许会抛异常
memset
(data_,
0,
length_);
}
~ByteArray()
{
if
(nullptr
!=
data_)
delete
data_;
}
unsigned
char*
data_;
int
length_;
private:
ByteArray(const
ByteArray&);
};
void
bar()
{
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
void
bar()
{
ByteArray*
ba
=
new
ByteArray(2048);
std::unique_ptr
holder
(ba);
/*
使用 */
...
}
//<
正确地被析构,没有内存泄漏
void
foo()
{
try
{
bar();
}
catch
(const
char*
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
with
interfaces;
with
Ada.Finalization;
package
lib
is
type
uchars
is
array(positive
range<>)
of
interfaces.unsigned_8;
type
uchars_p
is
access
uchars;
type
ByteArray
is
new
Ada.Finalization.Limited_Controlled
with
private;
function
Create(length
:
integer)
return
ByteArray;
private
type
ByteArray
is
new
Ada.Finalization.Limited_Controlled
with
record
length
:
integer;
data
:
uchars_p;
end
record;
overriding
procedure
Initialize
(This:
in
out
ByteArray);
overriding
procedure
Finalize
(This:
in
out
ByteArray);
end
lib;
--
lib.adb
with
Ada.Unchecked_Deallocation;
package
body
lib
is
use
Ada.Finalization;
function
Create(length
:
integer)
return
ByteArray
is
begin
if
length
<
0
then
put_line("Create");
return
ByteArray'(Limited_Controlled
with length => length,
data=> new uchars(1..length));
end
if;
return
ByteArray'(Limited_Controlled
with
length
=>
0,
data=>
null);
end
Create;
overriding
procedure
Initialize
(This:
in
out
ByteArray)
is
begin
put_line("Initialize");
this.length
:=
0;
this.data
:=
null;
end
Initialize;
overriding
procedure
Finalize
(This:
in
out
ByteArray)
is
procedure
free
is
new
Ada.Unchecked_Deallocation
(uchars,
uchars_p);
begin
put_line("Finalize");
if
(this.data
/=
null)
then
free(this.data);
end
if;
end
Finalize;
end
lib;
--
main.adb
with
lib;
use
lib;
procedure
main
is
K
:
ByteArray
:=
Create(10240);
C
:
ByteArray;
begin
null;
end
main;
|
– 输出如下
./main
Create
Initialize
Finalize
Finalize
另一种情况是对I/O资源的处理,当我们不再使用资源时,必须将资源归还给系统。
下面例子来自 wikipedia的RAII条目:
|
void
write_to_file
(const
std::string
&
message)
{
static
std::mutex
mutex;
std::lock_guard
lock(mutex);
std::ofstream
file("example.txt");
if
(!file.is_open())
throw
std::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。
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
|
//
student.h
class
Student
{
public:
explicit
Student(const
char*
name,
int
age);
~Student();
private:
///<
Pimpl
struct
Context;
Context*
const
context_;
friend
class
Teacher;
};
//
student_p.h
#include
"student.h"
struct
Student::Context
{
explicit
Context(const
char*
name,
int
age)
{
...
}
//<
实质的数据存储在这里
};
//
student.cpp
#include
"student_p.h"
Student::Student(const
char*
name,
int
age)
:
context_(new
Context(name,
age)
{}
...
|
尽管如此,我个人还是在面向开发应用的接口中会尽量使用Pimpl来维护API和ABI的一致性,除非Pimpl会引起显著的性能下降。