3 回答
TA贡献2036条经验 获得超8个赞
那么为什么不称为“使用堆栈触发清理”(UTSTTC :)?
RAII告诉你该怎么做:在构造函数中获取你的资源!我会添加:一个资源,一个构造函数。UTSTTC只是其中的一个应用,RAII更多。
资源管理很糟糕。在这里,资源是在使用后需要清理的任何东西。对许多平台上的项目进行的研究表明,大多数错误都与资源管理有关 - 而且在Windows上尤其糟糕(由于有许多类型的对象和分配器)。
在C ++中,由于异常和(C ++样式)模板的组合,资源管理特别复杂。如需了解引擎盖,请参阅GOTW8)。
C ++保证当且仅当构造函数成功时才调用析构函数。依靠这一点,RAII可以解决普通程序员可能甚至不知道的许多令人讨厌的问题。除了“每当我返回时我的局部变量将被销毁”之外,还有一些例子。
让我们从FileHandle
使用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; }}
如果构造失败(有例外),则不会调用其他成员函数 - 甚至是析构函数。
RAII避免在无效状态下使用对象。在我们使用对象之前,它已经让生活更轻松。
现在,让我们看看临时对象:
void CopyFileData(FileHandle source, FileHandle dest);void Foo(){ CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));}
要处理三种错误情况:无法打开文件,只能打开一个文件,可以打开这两个文件但复制文件失败。在非RAII实现中,Foo
必须明确处理所有三种情况。
即使在一个声明中获得多个资源,RAII也会释放已获取的资源。
现在,让我们聚合一些对象:
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!"; }}
的构造Logger
将失败original
的构造失败(因为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解决方案。随着时间的推移,我已经解决了这里记录的方法。
TA贡献1831条经验 获得超4个赞
那里有很好的答案,所以我只是添加了一些被遗忘的东西。
0. RAII是关于范围的
RAII是关于两者:
获取构造函数中的资源(无论什么资源),并在析构函数中取消它。
在声明变量时执行构造函数,并在变量超出范围时自动执行析构函数。
其他人已经回答了这个问题,所以我不会详细说明。
1.使用Java或C#编码时,您已使用RAII ...
MONSIEUR JOURDAIN:什么!当我说,“妮可,把我的拖鞋带给我,给我睡帽,”这是散文?
哲学硕士:是的,先生。
MONSIEUR JOURDAIN:四十多年来,我一直在讲述散文而不知道任何事情,我非常感谢你教我这个。
- 莫里哀:中产阶级绅士,第2幕,场景4
正如Jourdain先生用散文所做的那样,C#甚至Java人已经使用RAII,但却是隐藏的方式。例如,下面的Java代码(这是通过替换在C#编写的相同方式synchronized
用lock
):
void foo(){ // etc. synchronized(someObject) { // if something throws here, the lock on someObject will // be unlocked } // etc.}
...已经在使用RAII:互斥锁获取在关键字(synchronized
或lock
)中完成,取消将在退出范围时完成。
它的符号非常自然,即使是从未听说过RAII的人也几乎不需要解释。
C ++在Java和C#方面的优势在于可以使用RAII进行任何操作。例如,有没有直接内建等效的synchronized
,也没有lock
在C ++中,但我们仍然可以拥有它们。
在C ++中,它将写成:
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 ++宏):
void foo(){ // etc. LOCK(someObject) { // if something throws here, the lock on someObject will // be unlocked } // etc.}
2. RAII有其他用途
白兔:[唱歌]我迟到/我迟到/非常重要的约会。/没时间说“你好。” / 再见。/我迟到了,我迟到了,我迟到了。
- 爱丽丝梦游仙境(迪士尼版,1951年)
你知道什么时候会调用构造函数(在对象声明中),并且你知道何时会调用它相应的析构函数(在作用域的出口处),所以你可以用一行来编写几乎神奇的代码。欢迎来到C ++仙境(至少从C ++开发人员的角度来看)。
例如,您可以编写一个计数器对象(我将其作为练习)并仅通过声明其变量来使用它,就像上面使用的锁对象一样:
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#方式:
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}
3.为什么C ++缺乏finally
?
[SHOUTING]这是最后的倒计时!
- 欧洲:最后的倒计时(抱歉,我没有引号,这里...... :-)
该finally
子句在C#/ Java中用于处理范围退出时的资源处理(通过return
抛出异常或抛出异常)。
精明的规范读者会注意到C ++没有finally子句。这不是错误,因为C ++不需要它,因为RAII已经处理了资源处理。(相信我,编写C ++析构函数比编写正确的Java finally子句,甚至是C#的正确Dispose方法更容易)。
不过,有时,一个finally
条款会很酷。我们可以用C ++做吗?我们可以!再次使用RAII。
结论:RAII不仅仅是C ++中的哲学:它是C ++
RAII?这是C ++ !!!
- C ++开发人员愤怒的评论,被一位不起眼的斯巴达国王和他的300个朋友无耻地复制
当您在C ++中达到某种程度的经验时,就开发人员和析构函数自动执行而言,您开始考虑RAII。
你开始在思维范围,以及{
和}
人物成为在代码中最重要的人。
几乎所有东西都适合RAII:异常安全,互斥,数据库连接,数据库请求,服务器连接,时钟,操作系统句柄等,以及最后但并非最不重要的内存。
数据库部分是不可忽略的,因为,如果您接受支付价格,您甚至可以用“ 事务编程 ”方式编写,执行代码行和代码行,直到最终确定是否要提交所有更改,或者,如果不可能,将所有更改都还原(只要每行至少满足强异常保证)。(参见Herb的Sutter关于事务编程的文章的第二部分)。
就像拼图一样,一切都很合适。
RAII是C ++的重要组成部分,如果没有它,C ++就不可能是C ++。
这解释了为什么有经验的C ++开发人员如此迷恋RAII,以及为什么RAII是他们在尝试使用其他语言时首先搜索的内容。
它解释了为什么垃圾收集器本身就是一项非常出色的技术,从C ++开发人员的角度来看并不那么令人印象深刻:
RAII已经处理了GC处理的大多数案例
GC在纯托管对象上使用循环引用比RAII更好(通过智能使用弱指针缓解)
GC仍然限于内存,而RAII可以处理任何类型的资源。
如上所述,RAII可以做很多事情......
TA贡献1836条经验 获得超5个赞
- 3 回答
- 0 关注
- 626 浏览
添加回答
举报