理解术语和概念的含义 - RAII(资源获取即初始化)
C++ 开发人员能否给我们详细描述一下 RAII 是什么、为什么它很重要以及它是否与其他语言有任何相关性?
我确实知道一点。 我相信它代表“资源获取就是初始化”。 然而,这个名字并不符合我(可能不正确)对 RAII 的理解:我的印象是 RAII 是一种初始化堆栈上对象的方法,这样,当这些变量超出范围时,析构函数将自动被调用导致资源被清理。
那么为什么不称为“使用堆栈触发清理”(UTSTTC:)? 如何从那里到达“RAII”?
怎样才能在堆栈上创建一些东西来清理堆上的东西呢? 另外,是否存在无法使用 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?
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.
So why isn't that called "using the stack to trigger cleanup" (UTSTTC:)? How do you get from there to "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?
Thanks.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(10)
RAII 告诉您该怎么做:在构造函数中获取资源! 我想补充一点:一种资源,一种构造函数。 UTSTTC 只是其中的一种应用,RAII 的用途远不止于此。
资源管理很糟糕。在这里,资源是指使用后需要清理的任何东西。 对许多平台上的项目的研究表明,大多数错误都与资源管理有关 - 在 Windows 上尤其糟糕(由于对象和分配器的类型很多)。
在 C++ 中,由于异常和(C++ 风格)模板的组合,资源管理特别复杂。 如需深入了解,请参阅 GOTW8)。
C++ 保证当且仅当构造函数成功时才会调用析构函数。 依靠这一点,RAII 可以解决许多普通程序员甚至可能没有意识到的棘手问题。 以下是“每当我返回时我的局部变量都会被销毁”之外的一些示例。
让我们从一个使用 RAII 的过于简单的
FileHandle
类开始:如果构造失败(有例外),则不会调用任何其他成员函数(甚至析构函数)。
RAII 避免使用处于无效状态的对象。在我们使用对象之前,它就已经让我们的生活变得更加轻松。
现在,让我们看一下临时对象:
需要处理三种错误情况:无法打开文件、只能打开一个文件、两个文件都可以打开但复制文件失败。 在非 RAII 实现中,
Foo
必须显式处理所有三种情况。RAII 会释放已获取的资源,即使在一条语句中获取了多个资源。
现在,让我们聚合一些对象:
如果
original 的情况下,
的构造函数失败(因为Logger
的构造函数将失败filename1
无法打开),duplex
的构造函数失败(因为filename2
无法打开) ),或者写入 Logger 构造函数体内的文件失败。 在任何这些情况下,Logger
的析构函数都不会被调用 - 因此我们不能依赖Logger
的析构函数来释放文件。 但如果original
被构造,它的析构函数将在Logger
构造函数的清理期间被调用。RAII 简化了部分构建后的清理工作。
缺点:
缺点? 所有问题都可以通过 RAII 和智能指针解决;-)
当您需要延迟获取、将聚合对象推送到堆上时,RAII 有时会很笨重。
想象一下 Logger 需要一个
SetTargetFile(const char* target)
。 在这种情况下,仍然需要成为 Logger 成员的句柄需要驻留在堆上(例如,在智能指针中,以适当地触发句柄的销毁。)我从来不希望确实是垃圾收集。 当我从事 C# 工作时,有时我会感到一阵幸福,因为我不需要关心,但更重要的是,我怀念所有可以通过确定性破坏创建的很酷的玩具。 (使用 IDisposable 并不能解决问题。)
我有一个特别复杂的结构,可能会从 GC 中受益,其中“简单”智能指针会导致多个类的循环引用。 我们通过仔细平衡强指针和弱指针蒙混过关,但任何时候我们想要改变什么,我们都必须研究一张大的关系图。 GC 可能会更好,但某些组件持有应尽快释放的资源。
关于 FileHandle 示例的注释:它并不是完整的,只是一个示例 - 但结果是不正确的。 感谢 Johannes Schaub 的指出和 FredOverflow 将其转变为正确的 C++0x 解决方案。 随着时间的推移,我已经习惯了此处记录的方法。
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.
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).
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++ 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".
Let us start with an overly simplistic
FileHandle
class employing RAII:If construction fails (with an exception), no other member function - not even the destructor - gets called.
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:
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 releases resources that were acquired, even when multiple resources are acquired within one statement.
Now, let us aggregate some objects:
The constructor of
Logger
will fail iforiginal
's constructor fails (becausefilename1
could not be opened),duplex
's constructor fails (becausefilename2
could not be opened), or writing to the files insideLogger
's constructor body fails. In any of these cases,Logger
's destructor will not be called - so we cannot rely onLogger
's destructor to release the files. But iforiginal
was constructed, its destructor will be called during cleanup of theLogger
constructor.RAII simplifies cleanup after partial construction.
Negative points:
Negative points? All problems can be solved with RAII and smart pointers ;-)
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 ofLogger
, needs to reside on the heap (e.g. in a smart pointer, to trigger the handle's destruction appropriately.)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.)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.
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.
那里有很好的答案,所以我只是添加一些被遗忘的东西。
0. RAII 涉及范围
RAII 涉及两者:
其他人已经回答过了,我就不多说了。
1. 当使用 Java 或 C# 进行编码时,您已经使用了 RAII...
正如 Jourdain 先生对散文所做的那样,C# 甚至 Java 人已经使用 RAII,但以隐藏的方式例如,下面的 Java 代码(即:在 C# 中以相同的方式编写,将
synchronized
替换为lock
):... 已经使用 RAII:互斥锁获取是在关键字 (
synchronized 或
lock
),并且在退出作用域时取消获取。它的表示法非常自然,即使对于从未听说过 RAII 的人也几乎不需要解释
C++ 的优点 。与 Java 和 C# 相比,任何事情都可以使用 RAII 来实现,例如,在 C++ 中没有直接内置的
synchronized
或lock
,但我们仍然可以。拥有他们。在 C++ 中,可以这样写:
可以像在 Java/C# 中一样轻松编写(使用 C++ 宏):
2. RAII 有其他用途
您知道何时调用构造函数(在对象声明处),并且您知道何时调用其相应的析构函数(在作用域的出口处) ,因此您只需一行即可编写几乎神奇的代码。 欢迎来到 C++ 仙境(至少从 C++ 开发人员的角度来看)。
例如,您可以编写一个计数器对象(我将其作为练习)并仅通过声明其变量来使用它,就像使用上面的锁对象一样:
当然,可以再次使用 Java/C# 方式编写一个宏:
3.为什么C++缺少
finally
?finally
子句在 C#/Java 中用于处理资源处置范围退出的情况(通过返回
或抛出异常)。精明的规范读者会注意到 C++ 没有finally 子句。 这不是一个错误,因为 C++ 不需要它,因为 RAII 已经处理了资源处置。 (相信我,编写 C++ 析构函数比编写正确的 Java finally 子句甚至 C# 的正确 Dispose 方法要容易得多)。
不过,有时,
finally
子句还是很酷的。 我们可以用 C++ 来做吗? 是的,我们可以!再次交替使用 RAII。结论:RAII 不仅仅是 C++ 的哲学:它就是 C++
当您在 C++ 方面达到一定程度的经验时,您开始考虑 RAII, 构造函数和析构函数自动执行的术语。
您开始考虑范围,并且
{
和}
字符成为代码中最重要的字符。几乎所有内容都符合 RAII:异常安全、互斥体、数据库连接、数据库请求、服务器连接、时钟、操作系统句柄等,以及最后但并非最不重要的内存。
数据库部分是不可忽视的,因为,如果您愿意付出代价,您甚至可以以“事务性编程”风格进行编写,执行一行又一行的代码,直到最终决定是否您想要提交所有更改,或者,如果不可能,则恢复所有更改(只要每行至少满足强异常保证)。 (有关事务性编程,请参阅 Herb 的 Sutter 文章 的第二部分)。
就像拼图一样,一切都适合。
RAII 是 C++ 的重要组成部分,没有它,C++ 就不可能是 C++。
这解释了为什么经验丰富的 C++ 开发人员如此迷恋 RAII,以及为什么 RAII 是他们在尝试其他语言时首先搜索的内容。
它解释了为什么垃圾收集器虽然本身是一项伟大的技术,但从 C++ 开发人员的角度来看并不那么令人印象深刻:
There are excellent answers out there, so I just add some things forgotten.
0. RAII is about scopes
RAII is about both:
Others already answered about that, so I won't elaborate.
1. When coding in Java or C#, you already use RAII...
As Monsieur Jourdain did with prose, C# and even Java people already use RAII, but in hidden ways. For example, the following Java code (which is written the same way in C# by replacing
synchronized
withlock
):... is already using RAII: The mutex acquisition is done in the keyword (
synchronized
orlock
), and the un-acquisition will be done when exiting the scope.It's so natural in its notation it requires almost no explanation even for people who never heard about RAII.
The advantage C++ has over Java and C# here is that anything can be made using RAII. For example, there are no direct build-in equivalent of
synchronized
norlock
in C++, but we can still have them.In C++, it would be written:
which can be easily written as it would be in Java/C# (using C++ macros):
2. RAII have alternate uses
You know when the constructor will be called (at the object declaration), and you know when its corresponding destructor will be called (at the exit of the scope), so you can write almost magical code with but a line. Welcome to the C++ wonderland (at least, from a C++ developer's viewpoint).
For example, you can write a counter object (I let that as an exercise) and use it just by declaring its variable, like the lock object above was used:
which of course, can be written, again, the Java/C# way using a macro:
3. Why does C++ lack
finally
?The
finally
clause is used in C#/Java to handle resource disposal in case of scope exit (either through areturn
or a thrown exception).Astute specification readers will have noticed C++ has no finally clause. And this is not an error, because C++ does not need it, as RAII already handle resource disposal. (And believe me, writing a C++ destructor is magnitudes easier than writing the right Java finally clause, or even a C#'s correct Dispose method).
Still, sometimes, a
finally
clause would be cool. Can we do it in C++? Yes, we can! And again with an alternate use of RAII.Conclusion: RAII is a more than philosophy in C++: It's C++
When you reach some level of experience in C++, you start thinking in terms of RAII, in terms of construtors and destructors automated execution.
You start thinking in terms of scopes, and the
{
and}
characters become ones of the most important in your code.And almost everything fits right in terms of RAII: exception safety, mutexes, database connections, database requests, server connection, clocks, OS handles, etc., and last, but not least, memory.
The database part is not negligible, as, if you accept to pay the price, you can even write in a "transactional programming" style, executing lines and lines of code until deciding, in the end, if you want to commit all the changes, or, if not possible, having all the changes reverted back (as long as each line satisfy at least the Strong Exception Guarantee). (see the second part of this Herb's Sutter article for the transactional programming).
And like a puzzle, everything fits.
RAII is so much part of C++, C++ could not be C++ without it.
This explains why experienced C++ developers are so enamored with RAII, and why RAII is the first thing they search when trying another language.
And it explains why the Garbage Collector, while a magnificient piece of technology in itself, is not so impressive from a C++ developer's viewpoint:
RAII 使用 C++ 析构函数语义来管理资源。 例如,考虑一个智能指针。 您有一个指针的参数化构造函数,它使用对象的地址初始化该指针。 您在堆栈上分配一个指针:
当智能指针超出范围时,指针类的析构函数将删除连接的对象。 指针是堆栈分配的,对象是堆分配的。
在某些情况下,RAII 没有帮助。 例如,如果您使用引用计数智能指针(如 boost::shared_ptr)并创建一个带有循环的类似图形的结构,您将面临内存泄漏的风险,因为循环中的对象将阻止彼此被释放。 垃圾收集将有助于解决这个问题。
RAII is using C++ destructors semantics to manage resources. For example, consider a smart pointer. You have a parameterized constructor of the pointer that initializes this pointer with the adress of object. You allocate a pointer on stack:
When the smart pointer goes out of scope the destructor of the pointer class deletes the connected object. The pointer is stack-allocated and the object - heap-allocated.
There are certain cases when RAII doesn't help. For example, if you use reference-counting smart pointers (like boost::shared_ptr) and create a graph-like structure with a cycle you risk facing a memory leak because the objects in a cycle will prevent each other from being released. Garbage collection would help against this.
我想比之前的回应更强烈一些。
RAII,资源获取即初始化意味着所有获取的资源都应该在对象初始化的上下文中获取。 这禁止“裸”资源获取。 基本原理是 C++ 中的清理工作基于对象,而不是基于函数调用。 因此,所有清理工作都应该由对象完成,而不是函数调用。 从这个意义上说,C++ 比 Java 等更面向对象。 Java 清理基于
finally
子句中的函数调用。I'd like to put it a bit more strongly then previous responses.
RAII, Resource Acquisition Is Initialization means that all acquired resources should be acquired in the context of the initialization of an object. This forbids "naked" resource acquisition. The rationale is that cleanup in C++ works on object basis, not function-call basis. Hence, all cleanup should be done by objects, not function calls. In this sense C++ is more-object oriented then e.g. Java. Java cleanup is based on function calls in
finally
clauses.我同意 cpitis 的观点。 但想补充一点,资源可以是任何东西,而不仅仅是内存。 资源可以是文件、临界区、线程或数据库连接。
之所以称为资源获取即初始化,是因为在构造控制资源的对象时获取资源,如果构造函数失败(即由于异常),则不会获取资源。 然后,一旦对象超出范围,资源就会被释放。 c++保证堆栈上所有已成功构造的对象都将被破坏(这包括基类和成员的构造函数,即使超类构造函数失败)。
RAII 背后的原理是使资源获取异常安全。 无论异常发生在哪里,所有获取的资源都会被正确释放。 然而,这确实依赖于获取资源的类的质量(这必须是异常安全的,但这很难)。
I concur with cpitis. But would like to add that the resources can be anything not just memory. The resource could be a file, a critical section, a thread or a database connection.
It is called Resource Acquisition Is Initialization because the resource is acquired when the object controlling the resource is constructed, If the constructor failed (ie due to an exception) the resource is not acquired. Then once the object goes out of scope the resource is released. c++ guarantees that all objects on the stack that have been successfully constructed will be destructed (this includes constructors of base classes and members even if the super class constructor fails).
The rational behind RAII is to make resource acquisition exception safe. That all resources acquired are properly released no matter where an exception occurs. However this does rely on the quality of the class that acquires the resource (this must be exception safe and this is hard).
垃圾收集的问题在于您会失去对 RAII 至关重要的确定性破坏。 一旦变量超出范围,垃圾收集器就会决定何时回收该对象。 对象所持有的资源将继续保留,直到调用析构函数为止。
The problem with garbage collection is that you lose the deterministic destruction that's crucial to RAII. Once a variable goes out of scope, it's up to the garbage collector when the object will be reclaimed. The resource that's held by the object will continue to be held until the destructor gets called.
RAII 来自资源分配即初始化。 基本上,这意味着当构造函数完成执行时,构造的对象已完全初始化并可供使用。 它还意味着析构函数将释放该对象拥有的任何资源(例如内存、操作系统资源)。
与垃圾收集语言/技术(例如Java、.NET)相比,C++ 允许完全控制对象的生命周期。 对于堆栈分配的对象,您将知道何时调用该对象的析构函数(当执行超出范围时),这在垃圾收集的情况下不受真正控制。 即使在 C++ 中使用智能指针(例如 boost::shared_ptr),您也会知道,当没有对所指向对象的引用时,将调用该对象的析构函数。
RAII comes from Resource Allocation Is Initialization. Basically, it means that when a constructor finishes the execution, the constructed object is fully initialized and ready to use. It also implies that the destructor will release any resources (e.g. memory, OS resources) owned by the object.
Compared with garbage collected languages/technologies (e.g. Java, .NET), C++ allows full control of the life of an object. For a stack allocated object, you'll know when the destructor of the object will be called (when the execution goes out of the scope), thing that is not really controlled in case of garbage collection. Even using smart pointers in C++ (e.g. boost::shared_ptr), you'll know that when there is no reference to the pointed object, the destructor of that object will be called.
当 int_buffer 实例存在时,它必须有一个大小,并且它将分配必要的内存。 当它超出范围时,它的析构函数被调用。 这对于同步对象之类的事情非常有用。 考虑
不,不是真的。
绝不。 垃圾收集仅解决动态资源管理的一小部分。
When an instance of int_buffer comes into existence it must have a size, and it will allocate the necessary memory. When it goes out of scope, it's destructor is called. This is very useful for things like synchronization objects. Consider
No, not really.
Never. Garbage collection only solves a very small subset of dynamic resource management.
这里已经有很多很好的答案,但我想补充一点:
RAII 的一个简单解释是,在 C++ 中,只要超出范围,在堆栈上分配的对象就会被销毁。 这意味着,将调用对象析构函数并可以执行所有必要的清理。
这意味着,如果创建一个对象时没有“new”,则不需要“delete”。 这也是“智能指针”背后的想法 - 它们驻留在堆栈上,并且本质上包装了基于堆的对象。
There are already a lot of good answers here, but I'd just like to add:
A simple explanation of RAII is that, in C++, an object allocated on the stack is destroyed whenever it goes out of scope. That means, an objects destructor will be called and can do all necessary cleanup.
That means, if an object is created without "new", no "delete" is required. And this is also the idea behind "smart pointers" - they reside on the stack, and essentially wraps a heap based object.
RAII 是资源获取即初始化的缩写。
这种技术对于 C++ 来说非常独特,因为它们支持构造函数和构造函数。 析构函数和 几乎自动匹配传入参数的构造函数,或者最坏的情况是默认构造函数被称为 & 如果显式提供,则调用析构函数;否则,如果您没有为 C++ 类显式编写析构函数,则调用 C++ 编译器添加的默认析构函数。 这种情况仅发生在自动管理的 C++ 对象上 - 这意味着不使用自由存储(使用 new、new[]/delete、delete[] C++ 运算符分配/释放内存)。
RAII 技术利用这种自动管理对象功能来处理在堆/自由存储上创建的对象,方法是使用 new/new[] 显式请求更多内存,这些对象应通过调用 delete/delete[] 显式销毁。 自动管理对象的类将包装在堆/自由存储内存上创建的另一个对象。 因此,当运行自动管理对象的构造函数时,将在堆/自由存储内存和包装上创建包装对象。 当自动管理对象的句柄超出范围时,会自动调用该自动管理对象的析构函数,其中使用删除来销毁包装的对象。 使用 OOP 概念,如果您将此类对象包装在私有范围内的另一个类中,您将无法访问包装的类成员和对象。 方法与方法 这就是设计智能指针(又名句柄类)的原因。 这些智能指针将包装的对象作为类型化对象公开给外部世界和外部世界。 允许调用公开的内存对象组成的任何成员/方法。 请注意,智能指针根据不同的需求有不同的风格。 您应该参考 Andrei Alexandrescu 的 Modern C++ 编程或 boost 库 (www.boostorg) 的shared_ptr.hpp 实现/文档来了解更多信息。 希望这可以帮助您了解 RAII。
RAII is an acronym for Resource Acquisition Is Initialization.
This technique is very much unique to C++ because of their support for both Constructors & Destructors & almost automatically the constructors that are matching that arguments being passed in or the worst case the default constructor is called & destructors if explicity provided is called otherwise the default one that is added by the C++ compiler is called if you didn't write an destructor explicitly for a C++ class. This happens only for C++ objects that are auto-managed - meaning that are not using the free store (memory allocated/deallocated using new,new[]/delete,delete[] C++ operators).
RAII technique makes use of this auto-managed object feature to handle the objects that are created on the heap/free-store by explcitly asking for more memory using new/new[], which should be explicitly destroyed by calling delete/delete[]. The auto-managed object's class will wrap this another object that is created on the heap/free-store memory. Hence when auto-managed object's constructor is run, the wrapped object is created on the heap/free-store memory & when the auto-managed object's handle goes out of scope, destructor of that auto-managed object is called automatically in which the wrapped object is destroyed using delete. With OOP concepts, if you wrap such objects inside another class in private scope, you wouldn't have access to the wrapped classes members & methods & this is the reason why smart pointers (aka handle classes) are designed for. These smart pointers expose the wrapped object as typed object to external world & there by allowing to invoke any members/methods that the exposed memory object is made up of. Note that smart pointers have various flavors based on different needs. You should refer to Modern C++ programming by Andrei Alexandrescu or boost library's (www.boostorg) shared_ptr.hpp implementation/documentation to learn more about it. Hope this helps you to understand RAII.