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會引起顯著的性能下降。