理解术语和概念的含义 - RAII (Resource Acquisition is Initialization)
C++ 开发人员能否向我们详细描述一下 RAII 是什么,为什么它很重要,以及它是否可能与其他语言有任何关联?
Could you C++ developers please give us a good description of what RAII is, why it is important, and whether or not it might have any relevance to other languages?
我确实知道一点.我相信它代表资源获取就是初始化".然而,这个名字与我(可能不正确)对 RAII 的理解不符:我的印象是 RAII 是一种在堆栈上初始化对象的方式,这样,当这些变量超出范围时,析构函数将自动被调用导致资源被清理.
I do know a little bit. I believe it stands for "Resource Acquisition is Initialization". However, that name doesn't jive with my (possibly incorrect) understanding of what RAII is: I get the impression that RAII is a way of initializing objects on the stack such that, when those variables go out of scope, the destructors will automatically be called causing the resources to be cleaned up.
那为什么不称为使用堆栈触发清理"(UTSTTC:)?你如何从那里到达RAII"?
So why isn't that called "using the stack to trigger cleanup" (UTSTTC:)? How do you get from there to "RAII"?
你怎么能在堆栈上创建一些东西来清理堆上的东西呢?另外,是否存在不能使用 RAII 的情况?您是否曾经发现自己希望进行垃圾收集?至少一个垃圾收集器可以用于一些对象,同时让其他对象进行管理?
And how can you make something on the stack that will cause the cleanup of something that lives on the heap? Also, are there cases where you can't use RAII? Do you ever find yourself wishing for garbage collection? At least a garbage collector you could use for some objects while letting others be managed?
谢谢.
推荐答案
那为什么不叫使用堆栈触发清理"(UTSTTC:)?
So why isn't that called "using the stack to trigger cleanup" (UTSTTC:)?
RAII 告诉您该做什么:在构造函数中获取您的资源!我要补充:一种资源,一种构造函数.UTSTTC 只是其中的一种应用,RAII 远不止于此.
RAII is telling you what to do: Acquire your resource in a constructor! I would add: one resource, one constructor. UTSTTC is just one application of that, RAII is much more.
资源管理很烂.在这里,资源是指使用后需要清理的任何东西.对跨平台项目的研究表明,大多数错误都与资源管理有关 - 在 Windows 上尤其严重(由于对象和分配器的类型很多).
Resource Management sucks. Here, resource is anything that needs cleanup after use. Studies of projects across many platforms show the majority of bugs are related to resource management - and it's particularly bad on Windows (due to the many types of objects and allocators).
在 C++ 中,由于异常和(C++ 风格)模板的结合,资源管理特别复杂.如需深入了解,请参阅 GOTW8).
In C++, resource management is particularly complicated due to the combination of exceptions and (C++ style) templates. For a peek under the hood, see GOTW8).
C++ 保证当且仅当构造函数成功时才调用析构函数.依靠这一点,RAII 可以解决许多普通程序员甚至可能没有意识到的棘手问题.除了我的局部变量将在我返回时被销毁"之外,这里还有一些示例.
C++ guarantees that the destructor is called if and only if the constructor succeeded. Relying on that, RAII can solve many nasty problems the average programmer might not even be aware of. Here are a few examples beyond the "my local variables will be destroyed whenever I return".
让我们从一个使用 RAII 的过于简单的 FileHandle
类开始:
Let us start with an overly simplistic FileHandle
class employing RAII:
class FileHandle
{
FILE* file;
public:
explicit FileHandle(const char* name)
{
file = fopen(name);
if (!file)
{
throw "MAYDAY! MAYDAY";
}
}
~FileHandle()
{
// The only reason we are checking the file pointer for validity
// is because it might have been moved (see below).
// It is NOT needed to check against a failed constructor,
// because the destructor is NEVER executed when the constructor fails!
if (file)
{
fclose(file);
}
}
// The following technicalities can be skipped on the first read.
// They are not crucial to understanding the basic idea of RAII.
// However, if you plan to implement your own RAII classes,
// it is absolutely essential that you read on :)
// It does not make sense to copy a file handle,
// hence we disallow the otherwise implicitly generated copy operations.
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// The following operations enable transfer of ownership
// and require compiler support for rvalue references, a C++0x feature.
// Essentially, a resource is "moved" from one object to another.
FileHandle(FileHandle&& that)
{
file = that.file;
that.file = 0;
}
FileHandle& operator=(FileHandle&& that)
{
file = that.file;
that.file = 0;
return *this;
}
}
如果构造失败(有例外),则不会调用其他成员函数 - 甚至是析构函数 - 都不会被调用.
If construction fails (with an exception), no other member function - not even the destructor - gets called.
RAII 避免使用处于无效状态的对象.它在我们使用对象之前就已经让生活变得更轻松了.
RAII avoids using objects in an invalid state. it already makes life easier before we even use the object.
现在,让我们看看临时对象:
Now, let us have a look at temporary objects:
void CopyFileData(FileHandle source, FileHandle dest);
void Foo()
{
CopyFileData(FileHandle("C:\source"), FileHandle("C:\dest"));
}
需要处理三种错误情况:无法打开文件、只能打开一个文件、两个文件都可以打开但复制文件失败.在非 RAII 实现中,Foo
必须明确处理所有三种情况.
There are three error cases to handled: no file can be opened, only one file can be opened, both files can be opened but copying the files failed. In a non-RAII implementation, Foo
would have to handle all three cases explicitly.
RAII 释放已获取的资源,即使在一个语句中获取了多个资源.
现在,让我们聚合一些对象:
Now, let us aggregate some objects:
class Logger
{
FileHandle original, duplex; // this logger can write to two files at once!
public:
Logger(const char* filename1, const char* filename2)
: original(filename1), duplex(filename2)
{
if (!filewrite_duplex(original, duplex, "New Session"))
throw "Ugh damn!";
}
}
如果original
的构造函数失败,Logger
的构造函数会失败(因为filename1
无法打开),duplex
的构造函数失败(因为 filename2
无法打开),或者写入 Logger
构造函数主体内的文件失败.在任何这些情况下,Logger
的析构函数将 被调用 - 所以我们不能依赖 Logger
的析构函数来释放文件.但是如果构造了original
,那么它的析构函数会在Logger
构造函数的清理过程中被调用.
The constructor of Logger
will fail if original
's constructor fails (because filename1
could not be opened), duplex
's constructor fails (because filename2
could not be opened), or writing to the files inside Logger
's constructor body fails. In any of these cases, Logger
's destructor will not be called - so we cannot rely on Logger
's destructor to release the files. But if original
was constructed, its destructor will be called during cleanup of the Logger
constructor.
RAII 简化了部分构建后的清理工作.
负面因素:
负分?所有问题都可以通过 RAII 和智能指针解决 ;-)
Negative points? All problems can be solved with RAII and smart pointers ;-)
当您需要延迟获取时,RAII 有时会显得笨拙,将聚合对象推入堆中.
想象一下 Logger 需要一个 SetTargetFile(const char* target)
.在这种情况下,仍然需要成为 Logger
成员的句柄需要驻留在堆上(例如,在智能指针中,以适当地触发句柄的销毁.)
RAII is sometimes unwieldy when you need delayed acquisition, pushing aggregated objects onto the heap.
Imagine the Logger needs a SetTargetFile(const char* target)
. In that case, the handle, that still needs to be a member of Logger
, needs to reside on the heap (e.g. in a smart pointer, to trigger the handle's destruction appropriately.)
我从来没有真正希望垃圾收集.当我做 C# 时,我有时会感到一种幸福,我不需要在意,但更想念所有可以通过确定性破坏创造的酷玩具.(使用 IDisposable
只是不会削减它.)
I have never wished for garbage collection really. When I do C# I sometimes feel a moment of bliss that I just do not need to care, but much more I miss all the cool toys that can be created through deterministic destruction. (using IDisposable
just does not cut it.)
我有一个可能从 GC 中受益的特别复杂的结构,其中简单"的智能指针会导致对多个类的循环引用.我们通过仔细平衡强指针和弱指针来糊里糊涂,但任何时候我们想改变一些东西,我们都必须研究一个大的关系图.GC 可能会更好,但一些组件持有的资源应该尽快释放.
I have had one particularly complex structure that might have benefited from GC, where "simple" smart pointers would cause circular references over multiple classes. We muddled through by carefully balancing strong and weak pointers, but anytime we want to change something, we have to study a big relationship chart. GC might have been better, but some of the components held resources that should be release ASAP.
关于 FileHandle 示例的说明:它并不打算完整,只是一个示例 - 但结果不正确.感谢 Johannes Schaub 指出并感谢 FredOverflow 将其转变为正确的 C++0x 解决方案.随着时间的推移,我已经习惯了此处记录的方法.
A note on the FileHandle sample: It was not intended to be complete, just a sample - but turned out incorrect. Thanks Johannes Schaub for pointing out and FredOverflow for turning it into a correct C++0x solution. Over time, I've settled with the approach documented here.
相关文章