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条目:
1
2
3
4
5
6
7
8
|
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。
1234567891011121314151617181920212223242526// student.hclass Student{public :explicit Student ( const char * name , int age ) ;~ Student ( ) ;private :///< Pimplstruct 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会引起显著的性能下降。