您的位置:首页 > 大数据 > 人工智能

RAII与Pimpl

2013-11-12 19:40 246 查看
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。

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会引起显著的性能下降。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: