C++ 中的 RAII 机制
作者:gnuhpc
出处:http://www.cnblogs.com/gnuhpc/
Table of Contents
1.概念
Resource Acquisition Is Initialization 机制是 [Bjarne Stroustrup] 首先提出的。要解决的是这样一个问题:
在C++中,如果在这个程序段结束时需要完成一些资源释放工作,那么正常情况下自然是没有什么问题,但是当一个异常抛出时,释放资源的语句就不会被执行。 于是 [Bjarne Stroustrup] 就想到确保能运行资源释放代码的地方就是在这个程序段(栈帧)中放置的对象的析构函数了,因为 stack winding 会保证它们的析构函数都会被执行。
将初始化和资源释放都移动到一个包装类中的好处:
- 保证了资源的正常释放
- 省去了在异常处理中冗长而重复甚至有些还不一定执行到的清理逻辑,进而确保了代码的异常安全。
- 简化代码体积。
2.应用场景
1)文件操作
我们可以是用这个机制将文件操作包装起来完成一个异常安全的文件类。实现上,注意将复制构造函数和赋值符私有化,这个是通过一个私有继承类完成的,因为这两个操作在此并没有意义,当然这并不是RAII所要求的。
class NonCopyable {
public:
NonCopyable() {};
private:
NonCopyable(NonCopyable const &); // private copy constructor
NonCopyable& operator=(NonCopyable const &); // private assignment operator
};
class SafeFile:NonCopyable {
public:
SafeFile(const char* filename) : fileHandler(fopen(filename, "w+")) {
if (fileHandler == NULL) {
throw runtime_error("fopen");
}
}
~SafeFile() { fclose(fileHandler); }
void write(const char* str) {
if (fputs(str,fileHandler) == EOF) { throw runtime_error("fputs"); }
}
void write(const char* buffer, size_t num) {
if (num != 0 && fwrite(buffer,num,1,fileHandler) == 0) {
throw runtime_error("fwrite");
}
}
private:
FILE *fileHandler;
SafeFile(const SafeFile&);
SafeFile &operator =(const SafeFile&);
};
int main(int argc, char *argv[]) {
SafeFile testFile("foo.test");
testFile.write("Hello RAII");
}
C++ 的结构决定了其原生支持 RAII,而在 Java 中,对象何时销毁是未知的,所以在 Java 中可以使用 try-finally 做相关处理。
2)智能指针模拟
一个更复杂一点的例子是模拟智能指针,抽象出来的 RAII 类中实现了一个操作符 *
,直接返回存入的指针:
现在我们有一个类:
class Example {
SomeResource* p_;
SomeResource* p2_;
public:
Example() : p_(new SomeResource()), p2_(new SomeResource()) {
std::cout << "Creating Example, allocating SomeResource!/n";
}
Example(const Example& other) :
p_(new SomeResource(*other.p_)),
p2_(new SomeResource(*other.p2_)) {}
Example& operator=(const Example& other) {
// self assignment?
if (this == &other)
return *this;
*p_=*other.p_;
*p2_=*other.p2_;
return *this;
}
~Example() {
std::cout << "Deleting Example, freeing SomeResource!/n";
delete p_;
delete p2_;
}
};
假设在创建 SomeResource 的时候可能会有异常,那么当 p_ 指向的资源被创建但 p2_ 指向的资源创建失败时,Example 的实例就整个创建失败,那么 p_ 指向的资源就存在内存泄露问题。
用下边的这个方法可以为权宜之计:
Example() : p_(0),p2_(0) {
try {
p_=new SomeResource();
p2_=new SomeResource("H",true);
std::cout << "Creating Example, allocating SomeResource!/n";
}
catch(...) {
delete p2_;
delete p_;
throw;
}
}
但是我们可以利用一个对象在离开一个域中会调用析构函数的特性,在构造函数中完成初始化,在析构函数中完成清理工作,将需要操作和保护的指针作为成员变量放入 RAII 中。
template
class RAII {
T* p_;
public:
explicit RAII(T* p) : p_(p) {}
~RAII() { delete p_; }
void reset(T* p) { delete p_; p_=p; }
T* get() const { return p_; }
T& operator*() const { return *p_; }
void swap(RAII& other) { std::swap(p_,other.p_); }
private:
RAII(const RAII& other);
RAII& operator=(const RAII& other);
};
我们在具体使用把保护的指针 Someresource 放在RAII中:
class Example {
RAII p_;
RAII p2_;
public:
Example() :
p_(new SomeResource()),
p2_(new SomeResource()) {}
Example(const Example& other)
: p_(new SomeResource(*other.p_)),
p2_(new SomeResource(*other.p2_)) {}
Example& operator=(const Example& other) {
// self assignment?
if (this == &other) return *this;
*p_=*other.p_;
*p2_=*other.p2_;
return *this;
}
~Example() {
std::cout << "Deleting Example, freeing SomeResource!/n";
}
};
现在即使 p_ 成功而 p2_ 失败,那么在 Stack winding 时也会调用 RAII 的析构函数保证了 p_ 指向的 Someresource 被析构。
这种方法较之例1中需要实现被组合的指针类型相应的接口不同,这里不需要对接口进行封装。
当然,在例1中,你也可以提供一个 getPointer 的函数直接将句柄提供出来。
其实在 Example 中,已经不需要析构函数了,因为 RAII 类会帮它照顾好这一切的。这有点像 auto_ptr,本文并不打算深入讨论智能指针这个话题。
3)锁操作
int counter = 0;
void* routine(void *ptr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
class NonCopyable {
public:
NonCopyable() {};
private:
NonCopyable (NonCopyable const &); // private copy constructor
NonCopyable & operator = (NonCopyable const &); // private assignment operator
};
class ScopeMutex:NonCopyable {
public:
ScopeMutex(pthread_mutex_t* mutex):mutex_(mutex){
pthread_mutex_lock( mutex_ );
}
~ScopeMutex() {
pthread_mutex_unlock( mutex_ );
}
private:
pthread_mutex_t *mutex_;
};
int main(int argc, char *argv[]) {
int rc1, rc2;
pthread_t thread1, thread2;
if((rc1=pthread_create(&thread1, NULL, routine, NULL))) {
printf("Thread creation failed: %d/n", rc1);
}
if((rc2=pthread_create(&thread2, NULL, routine, NULL))) {
printf("Thread creation failed: %d/n", rc1);
}
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
}
void* routine(void *ptr) {
ScopeMutex scopeMutex(&mutex);
counter++;
printf("%d/n",counter);
}
3.总结
RAII 机制保证了异常安全,并且也为程序员在编写动态分配内存的程序时提供了安全保证。
缺点是有些操作可能会抛出异常,如果放在析构函数中进行则不能将错误传递出去,那么此时析构函数就必须自己处理异常。这在某些时候是很繁琐的。
4.参考文献
http://www.codeproject.com/KB/cpp/RAIIFactory.aspx 这篇文章用工厂方法的方式完成了一个RAII工厂。
http://www.informit.com/articles/printerfriendly.aspx?p=21084 讨论了异常安全的一些情况,其中提到赋值符的安全值得注意。
Bjarne Stroustrup: http://en.wikipedia.org/wiki/Bjarne_Stroustrup