Understanding the meaning of the term and the concept - RAII (Resource Acquisition is Initialization)
请C++开发人员给我们描述一下RAII是什么,为什么它是重要的,以及它是否可能与其他语言有任何关联?
我知道一点。我相信它代表着"资源获取就是初始化"。然而,这个名称并不违背我对raii的理解(可能不正确):我认为raii是一种初始化堆栈上对象的方法,当这些变量超出范围时,将自动调用析构函数,从而清理资源。
那么,为什么不称之为"使用堆栈触发清理"(utsttc:)。你怎么从那里到"raii"?
你怎么能在堆上做一些东西,来清理堆上的东西呢?还有,你不能使用RAII的情况吗?你有没有想过要收集垃圾?至少可以为一些对象使用垃圾收集器,同时允许管理其他对象?
谢谢。
So why isn't that called"using the stack to trigger cleanup" (UTSTTC:)?
RAII告诉你该怎么做:在一个构造函数中获取你的资源!我要补充:一个资源,一个构造函数。UTSTTC只是其中的一个应用,RAII更是如此。
资源管理很差劲。在这里,资源是任何在使用后需要清理的东西。对跨多个平台的项目的研究表明,大多数错误都与资源管理有关,而且在Windows上尤其糟糕(由于对象和分配器的类型很多)。
在C++中,由于异常和(C++样式)模板的组合,资源管理尤其复杂。要查看引擎盖下的信息,请参阅gotw8)。
C++保证,只要构造函数成功,就调用析构函数。基于此,RAII可以解决普通程序员甚至不知道的许多棘手问题。除了"我的局部变量将在每次返回时被销毁"之外,这里还有几个例子。
让我们从使用RAII的过于简单的
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 | 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; } } |
如果构造失败(异常),则不会调用其他成员函数,甚至不会调用析构函数。
RAII避免使用处于无效状态的对象。在我们使用这个物体之前,它已经让我们的生活更容易了。
现在,让我们来看看临时对象:
1 2 3 4 5 6 | void CopyFileData(FileHandle source, FileHandle dest); void Foo() { CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest")); } |
有三个错误情况需要处理:不能打开文件,只能打开一个文件,两个文件都可以打开,但复制文件失败。在非RAII实现中,
RAII释放被获取的资源,即使在一个语句中获取了多个资源。
现在,让我们聚合一些对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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!"; } } |
如果
RAII简化了部分施工后的清理。
否定点:
消极点?所有问题都可以用RAII和智能指针解决;-)
当您需要延迟采集时,RAII有时会很笨拙,将聚合的对象推送到堆中。假设记录器需要一个
我从来没有真正希望收集垃圾。当我这样做的时候,我有时会感到一种幸福的时刻,我不需要关心,但我更怀念通过确定性破坏所能创造出的所有酷玩具。(使用
我有一个特别复杂的结构,它可能从GC中受益,"简单"的智能指针将导致多个类上的循环引用。我们通过仔细平衡强弱指针来应付,但是每当我们想要改变什么,我们就必须研究一个大的关系图。GC可能更好,但一些组件保留了应该尽快发布的资源。
关于filehandle示例的一个注释:它不打算是完整的,只是一个示例-但结果是不正确的。感谢Johannes Schaub指出,FredOverflow把它变成一个正确的C++ 0x解决方案。随着时间的推移,我已经确定了这里记录的方法。
有很好的答案,所以我只是添加了一些被遗忘的东西。好的。0。RAII是关于范围的
RAII涉及两个方面:好的。
其他人已经回答了这个问题,所以我不会详细说明。好的。1。当用Java或C语言编码时,你已经使用了RAII…
MONSIEUR JOURDAIN: What! When I say,"Nicole, bring me my slippers,
and give me my nightcap," that's prose?Ok.
PHILOSOPHY MASTER: Yes, Sir.
Ok.
MONSIEUR JOURDAIN: For more than forty years I have been speaking prose without knowing anything about it, and I am much obliged to you for having taught me that.
Ok.
— Molière: The Middle Class Gentleman, Act 2, Scene 4
Ok.
正如Monsieur Jourdain对散文所做的那样,C·甚至Java人已经使用了RAII,但是隐藏起来了。例如,下面的Java代码(用EDCOX1(0)代替EDCOX1,1),用C语言编写同样的方法:好的。
1 2 3 4 5 6 7 8 9 10 11 12 | void foo() { // etc. synchronized(someObject) { // if something throws here, the lock on someObject will // be unlocked } // etc. } |
…已经在使用RAII:互斥采集是在关键字(
这是如此自然的符号,它几乎不需要任何解释,即使是对那些从来没有听说过RAII的人。好的。
C++的优势在于Java和C,这里的任何东西都可以使用RAII来实现。例如,在C++中,没有直接的Endox1×0Ω和EDCOX1 1Ω的等效构建,但我们仍然可以拥有它们。好的。
在C++中,它将被写成:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void foo() { // etc. { Lock lock(someObject) ; // lock is an object of type Lock whose // constructor acquires a mutex on // someObject and whose destructor will // un-acquire it // if something throws here, the lock on someObject will // be unlocked } // etc. } |
可以很容易地编写Java/C语言方式(使用C++宏):好的。
1 2 3 4 5 6 7 8 9 10 11 12 | void foo() { // etc. LOCK(someObject) { // if something throws here, the lock on someObject will // be unlocked } // etc. } |
2。RAII有其他用途
WHITE RABBIT: [singing] I'm late / I'm late / For a very important date. / No time to say"Hello." / Goodbye. / I'm late, I'm late, I'm late.
Ok.
— Alice in Wonderland (Disney version, 1951)
Ok.
您知道何时调用构造函数(在对象声明中),也知道何时调用其相应的析构函数(在作用域的出口处),所以只需一行代码就可以编写几乎不可思议的代码。欢迎来到C++仙境(至少从C++开发者的角度来看)。好的。
例如,您可以编写一个计数器对象(我让它作为练习)并通过声明其变量来使用它,就像上面使用的锁对象一样:好的。
1 2 3 4 5 6 7 8 9 10 11 | void foo() { double timeElapsed = 0 ; { Counter counter(timeElapsed) ; // do something lengthy } // now, the timeElapsed variable contain the time elapsed // from the Counter's declaration till the scope exit } |
当然,可以再次使用宏编写Java/C方式:好的。
1 2 3 4 5 6 7 8 9 10 11 | void foo() { double timeElapsed = 0 ; COUNTER(timeElapsed) { // do something lengthy } // now, the timeElapsed variable contain the time elapsed // from the Counter's declaration till the scope exit } |
三。为什么C++缺少EDCOX1?6?< Buff行情>
[喊叫]这是最后的倒计时!好的。
-欧洲:最后的倒计时(对不起,我没有报价,这里……-)好的。< /块引用>
EcOX1的0Ω子句用于C/J/Java处理范围内的资源处置(通过EDCOX1、1或抛出异常)。好的。
精明的规范读者会注意到C++没有最终子句。这不是一个错误,因为C++不需要它,因为RAII已经处理了资源处理。(相信我,编写C++析构函数比写入正确的Java最终子句更容易,甚至C语言的正确处理方法)。好的。
不过,有时,
RAII? THIS IS C++!!!
Ok.
— C++ developer's outraged comment, shamelessly copied by an obscure Sparta king and his 300 friends
Ok.
当你在C++中达到某种程度的经验时,你就开始考虑RAII,在编译器和析构函数的自动执行方面。好的。
您开始从范围的角度考虑问题,
几乎所有东西都适合RAII:异常安全、互斥锁、数据库连接、数据库请求、服务器连接、时钟、操作系统句柄等,最后,但不是最不重要的,内存。好的。
数据库部分不可忽略,因为如果您接受支付价格,您甚至可以用"事务性编程"的方式编写,执行行和代码行,直到最后决定是否要提交所有更改,或者如果不可能,将所有更改还原回来(只要每行至少满足强异常n保证)。(有关事务性编程,请参阅本Herb的Sutter文章的第二部分)。好的。
就像拼图一样,一切都很合适。好的。
RAII是C++的一部分,没有C++,C++就不能成为C++。好的。
这解释了为什么有经验的C++开发者如此热衷于RAII,以及为什么RAII是他们在尝试另一种语言时搜索的第一件事。好的。
它解释了为什么垃圾收集器本身是一个巨大的技术,但从C++开发者的观点来看,它并不是那么令人印象深刻:好的。
- RAII已经处理了GC处理的大部分案件
- GC比RAII更好地处理纯托管对象上的循环引用(通过智能地使用弱指针来缓解)
- GC仍然局限于内存,而RAII可以处理任何类型的资源。
- 如上所述,RAII可以做很多,更多…
好啊。
请参阅:
除了C++之外,其他语言的程序员是否使用、了解或理解RAII?
C++中的RAII和智能指针
C++支持"最后"块吗?(我一直听到的这个"raii"是什么?)
RAII与例外
等。。
RAII使用C++析构函数语义来管理资源。例如,考虑一个智能指针。您有一个指针的参数化构造函数,它用对象地址初始化此指针。在堆栈上分配指针:
1 | SmartPointer pointer( new ObjectClass() ); |
当智能指针超出范围时,指针类的析构函数将删除连接的对象。指针是堆栈分配的,对象是堆分配的。
有些情况下,RAII不起作用。例如,如果使用引用计数智能指针(如boost::shared_ptr),并创建一个具有循环的类似图的结构,则可能面临内存泄漏的风险,因为循环中的对象将阻止彼此释放。垃圾收集将有助于防止这种情况。
我想比以前的回答更强烈一点。
资源获取是初始化,是指所有获取的资源都应该在对象初始化的上下文中获取。这禁止"裸"获取资源。其原理是C++中的清理工作在对象基础上,而不是函数调用基础上。因此,所有清理都应该由对象来完成,而不是由函数调用来完成。从这个意义上讲,C++是面向对象的,例如Java。Java清理是基于EDCOX1的0个子句中的函数调用。
我同意cpitis。但要补充的是,资源可以是任何东西,而不仅仅是内存。资源可以是文件、关键部分、线程或数据库连接。
这称为资源获取初始化,因为资源是在构造控制资源的对象时获取的,如果构造函数失败(即由于异常),则不会获取资源。一旦对象超出范围,资源就会被释放。C++保证了已成功构建的堆栈上的所有对象将被破坏(这包括基类和成员的构造函数,即使超级类构造函数失败)。
RAII背后的理性是使资源获取异常安全。无论异常发生在哪里,都会正确地释放所有获得的资源。但是,这确实依赖于获取资源的类的质量(这必须是异常安全的,而且这很难实现)。
垃圾收集的问题是,您会丢失对raii至关重要的确定性破坏。一旦一个变量超出范围,当对象被回收时,它就由垃圾收集器决定。对象持有的资源将继续保持,直到调用析构函数。
RAII来自资源分配初始化。基本上,它意味着当一个构造函数完成执行时,被构造的对象被完全初始化并准备好使用。它还意味着析构函数将释放对象拥有的任何资源(例如内存、操作系统资源)。
与垃圾收集语言/技术(例如Java、.NET)相比,C++允许完全控制对象的生命。对于堆栈分配的对象,您将知道何时调用对象的析构函数(当执行超出范围时),这是在垃圾收集的情况下实际上不受控制的事情。即使使用C++中的智能指针(例如:Booo::SyrdY-PTR),您也会知道,当没有指向指向对象的引用时,将调用该对象的析构函数。
And how can you make something on the stack that will cause the cleanup of something that lives on the heap?
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 | class int_buffer { size_t m_size; int * m_buf; public: int_buffer( size_t size ) : m_size( size ), m_buf( 0 ) { if( m_size > 0 ) m_buf = new int[m_size]; // will throw on failure by default } ~int_buffer() { delete[] m_buf; } /* ...rest of class implementation...*/ }; void foo() { int_buffer ib(20); // creates a buffer of 20 bytes std::cout << ib.size() << std::endl; } // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed. |
当一个int_缓冲区的实例出现时,它必须有一个大小,并且它将分配必要的内存。当它超出范围时,调用它的析构函数。这对于像同步对象这样的事情非常有用。考虑
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 | class mutex { // ... take(); release(); class mutex::sentry { mutex & mm; public: sentry( mutex & m ) : mm(m) { mm.take(); } ~sentry() { mm.release(); } }; // mutex::sentry; }; mutex m; int getSomeValue() { mutex::sentry ms( m ); // blocks here until the mutex is taken return 0; } // the mutex is released in the destructor call here. |
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?
从未。垃圾收集只解决了动态资源管理中非常小的一个子集。
这里已经有很多好的答案了,但我想补充一下:RAII的一个简单解释是,在C++中,堆栈上分配的对象在超出范围时被销毁。这意味着,将调用对象析构函数并可以执行所有必要的清理。这意味着,如果创建的对象没有"新建",则不需要"删除"。这也是"智能指针"背后的想法——它们驻留在堆栈上,并且基本上包装了一个基于堆的对象。
RAII是资源获取初始化的缩写。
此技术对于C++非常独特,因为它们支持构造函数和析构函数;几乎自动地构造与正在传入的参数匹配的构造函数或最坏的情况下,如果提供了显式性,则调用默认构造函数和析构函数,否则调用默认构造函数。如果没有为C++类显式写析构函数,则调用C++编译器。这种情况只发生在自动管理的C++对象上,也就是说,不使用空闲存储(使用新的、新的[]/DELATE、Dele][C++运算符)分配/释放的内存。
RAII技术利用此自动管理的对象功能,通过使用new/new[]明确请求更多内存来处理堆/空闲存储上创建的对象,该内存应通过调用delete/delete[]明确销毁。自动管理对象的类将包装在堆/可用存储内存上创建的另一个对象。因此,当自动托管对象的构造函数运行时,将在堆/空闲存储内存中创建包装的对象;当自动托管对象的句柄超出范围时,将自动调用该自动托管对象的析构函数,在该函数中使用delete销毁包装的对象。对于OOP概念,如果将这些对象包装在私有范围内的另一个类中,则无法访问包装的类成员和方法,这就是智能指针(也称为句柄类)设计的原因。这些智能指针通过允许调用公开内存对象所组成的任何成员/方法,将包装的对象作为类型化对象公开给外部世界。请注意,根据不同的需求,智能指针具有不同的风格。您应该参考Andrei Alexandrescu或Boost库(www. BooStug)SyrdJ.pRT.HPP实现/文档的现代C++编程,以了解更多有关它的内容。希望这能帮助你理解雷伊。