理解术语和概念的含义-RAII (RAII)

请你们 C + + 开发人员给我们一个好的描述什么是 RAII,为什么它是重要的,它是否可能有任何相关性其他语言?

我知道一点。我相信它代表“ RAII”。然而,这个名字与我对什么是 RAII 的理解(可能是错误的)并不一致: 我得到的印象是,RAII 是一种初始化堆栈上对象的方法,当这些变量超出作用域时,析构函数将被自动调用,从而导致资源被清理。

那么,为什么不称之为“使用堆栈触发清理”(UTSTTC:) ? 如何从那里到“ RAII”?

那么如何在堆上创建一些东西,从而清理堆上的东西呢?还有,是否存在不能使用 RAII 的情况?你有没有发现自己希望收集垃圾?至少可以为某些对象使用垃圾收集器,同时让其他对象得到管理?

Thanks.

17942 次浏览

RAII 使用 C + + 析构函数语义来管理资源。例如,考虑智能指针。您有一个指针的参数化构造函数,该构造函数用对象的地址初始化此指针。在堆栈上分配一个指针:

SmartPointer pointer( new ObjectClass() );

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.

在某些情况下,RAII 没有帮助。例如,如果您使用引用计数智能指针(如 ost: : share _ ptr)并创建一个具有循环的图形结构,那么您将面临内存泄漏的风险,因为循环中的对象将阻止彼此被释放。收集垃圾有助于解决这个问题。

垃圾收集的问题在于,您失去了对 RAII 至关重要的确定性破坏。一旦一个变量超出了作用域范围,那么什么时候回收该对象取决于垃圾收集器。对象持有的资源将继续保留,直到调用析构函数为止。

RAII 来自资源分配初始化。基本上,它意味着当构造函数完成执行时,构造的对象已经完全初始化并可以使用了。它还意味着析构函数将释放对象所拥有的任何资源(例如内存、操作系统资源)。

与垃圾收集的语言/技术(例如 Java,。NET) ,C + + 允许完全控制对象的生命周期。对于堆栈分配的对象,您将知道什么时候将调用对象的析构函数(当执行超出作用域时) ,在垃圾收集的情况下不会真正控制它。即使在 C + + 中使用智能指针(例如,Boost: : share _ ptr) ,您也会知道当没有对指向的对象的引用时,将调用该对象的析构函数。

我同意黄疸。但要补充的是,资源可以是任何东西,而不仅仅是内存。资源可以是文件、关键部分、线程或数据库连接。

之所以称为 RAII,是因为在构造控制资源的对象时获取了该资源。如果构造函数失败(即由于异常) ,则不获取该资源。然后,一旦对象超出作用域,资源就被释放。C + + 保证堆栈上所有已成功构造的对象都将被销毁(这包括基类和成员的构造函数,即使超类构造函数失败)。

RAII 背后的理由是使资源获取异常安全。无论异常发生在何处,所有获取的资源都被正确释放。然而,这确实依赖于获取资源的类的质量(这必须是异常安全的,而且这很难)。

我想说得比之前的回答更强烈一点。

RAII,RAII意味着所有获取的资源都应该在对象初始化的上下文中获取。这禁止“赤裸裸的”资源获取。其基本原理是,C + + 中的清理工作基于对象,而不是基于函数调用。因此,所有的清理工作都应该由对象来完成,而不是函数调用。从这个意义上说,C + + 比 Java 更加面向对象。Java 清理基于 finally子句中的函数调用。

RAII 是 RAII 的首字母缩写。

这个技术对于 C + + 来说是独一无二的,因为他们同时支持构造函数和析构函数,并且几乎自动匹配传入的参数的构造函数,或者在最糟糕的情况下,这个缺省构造函数被称为 & 析构函数,如果被调用的是显式的,否则,如果你没有为一个 C + + 类写一个显式的析构函数,那么由 C + + 编译器添加的默认的被调用。这种情况只会发生在自动管理的 C + + 对象上——这意味着不使用空闲存储(使用 new、 new []/delete、 delete [] C + + 操作符分配/释放内存)。

RAII 技术利用这种自动管理对象特性,通过使用 new/new []显式请求更多内存来处理在堆/空闲存储器上创建的对象,这些内存应该通过调用 delete/delete []显式销毁。自动管理对象的类将包装在堆/可用存储内存中创建的另一个对象。因此,当自动管理对象的构造函数运行时,包装对象在堆/自由存储内存中创建; 当自动管理对象的句柄超出范围时,自动调用自动管理对象的析构函数,其中使用 delete 销毁包装对象。使用 OOP 概念,如果你将这些对象封装在另一个类的私有作用域中,你将无法访问封装的类成员和方法,这就是为什么智能指针(又名句柄类)被设计为。这些智能指针通过允许调用公开的内存对象所包含的任何成员/方法,将包装的对象作为类型化对象公开给外部世界 & there。注意,智能指针根据不同的需求有不同的风格。你应该通过 Andrei Alexandrescu 或者 boostorg 的 share _ ptr 来参考现代 C + + 编程。Hpp 实现/文档以了解更多信息。希望这能帮助你理解 RAII。

There are already a lot of good answers here, but I'd just like to add:
RAII 的一个简单解释是,在 C + + 中,每当堆栈上分配的对象超出作用域时,就会销毁该对象。这意味着,将调用一个对象析构函数并执行所有必要的清理。
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.

那么,为什么不称之为“使用堆栈触发清理”(UTSTTC:) ?

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).

在 C + + 中,由于异常和(C + + 样式)模板的组合,资源管理特别复杂。要了解引擎盖下的情况,请参阅 GOTW8)。


C + + 保证构造函数被称为构造函数继承的 如果,也只有如果。依赖于此,RAII 可以解决许多普通程序员可能甚至没有意识到的棘手问题。除了“每次返回时,我的局部变量都会被销毁”之外,还有几个例子。

让我们从一个使用 RAII 的过于简单化的 FileHandle类开始:

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 避免在无效状态下使用对象。在我们使用对象之前,它就已经使生活变得更容易了。

现在,让我们来看一下临时对象:

void CopyFileData(FileHandle source, FileHandle dest);


void Foo()
{
CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}

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 释放已获取的资源,即使在一个语句中获取了多个资源。

现在,让我们聚合一些对象:

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的构造函数失败(因为 filename1无法打开) ,duplex的构造函数失败(因为 filename2无法打开) ,或者写入 Logger构造函数体内的文件失败,那么 Logger的构造函数将失败。在这些情况下,Logger的析构函数将被调用 original0-所以我们不能依赖于 Logger的析构函数来释放文件。但是,如果构造了 original,那么在清理 Logger构造函数期间将调用它的析构函数。

RAII simplifies cleanup after partial construction.


负面观点:

负面观点? 所有问题都可以用 RAII 和智能指针解决; -)

当您需要延迟获取时,RAII 有时是笨拙的,它会将聚合对象推到堆上。
假设记录器需要一个 SetTargetFile(const char* target)。在这种情况下,仍然需要作为 Logger成员的句柄需要驻留在堆上(例如,在智能指针中,以适当地触发句柄的销毁)

我真的从来没有希望垃圾收集。当我做 C # 的时候,我有时会感到一阵幸福,因为我根本不需要在意,但是我更怀念那些可以通过确定性破坏创造出来的很酷的玩具。(使用 IDisposable并不能达到预期效果。)

我曾经有过一个特别复杂的结构,它可能受益于 GC,在 GC 中,“简单”智能指针会在多个类上引起循环引用。我们小心翼翼地权衡强弱,勉强度日,但每当我们想要改变什么时,我们就必须研究一个庞大的关系图表。GC 可能更好,但是有些组件拥有应该尽快发布的资源。


关于 FileHandle 示例的注意事项: 它并不打算是完整的,只是一个示例-但结果是不正确的。感谢 Johannes Schaub 指出,并感谢 FredOverflow 将其转化为正确的 C + + 0x 解决方案。随着时间的推移,我已经解决了方法 记录在案

那么如何在堆上创建一些东西,从而清理堆上的东西呢?

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.

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

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?

不,没有。

你有没有发现自己希望收集垃圾?至少可以为某些对象使用垃圾收集器,同时让其他对象得到管理?

从来没有。垃圾收集只解决了动态资源管理的一个很小的子集。

这里有很多很棒的答案,所以我只是补充了一些被遗忘的东西。

0. RAII is about scopes

RAII 是关于两者的:

  1. 在构造函数中获取一个资源(不管是什么资源) ,并在析构函数中取消获取它。
  2. 在声明变量时执行构造函数,当变量超出作用域时自动执行析构函数。

其他人已经回答过这个问题了,所以我就不多说了。

1. 当用 Java 或 C # 编写代码时,您已经使用了 RAII..。

什么! 当我说“妮可,把我的拖鞋拿来” “把我的睡前酒给我”这是散文吗?

是的,先生。

约尔丹先生: 四十多年来,我一直在说散文,对它一无所知,我非常感激你教我这一点。

— Molière: The Middle Class Gentleman, Act 2, Scene 4

正如 Jourdain 先生对散文所做的那样,C # ,甚至 Java 用户已经在使用 RAII 了,但是是以隐藏的方式。例如,下面的 Java 代码(在 C # 中用同样的方式编写,用 lock替换 synchronized) :

void foo()
{
// etc.


synchronized(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}


// etc.
}

... 已经在使用 RAII: 在关键字(synchronizedlock)中完成互斥锁获取,并且在退出作用域时完成取消获取。

它的符号非常自然,甚至对于那些从未听说过 RAII 的人来说,它几乎不需要任何解释。

与 Java 和 C # 相比,C + + 的优势在于任何东西都可以用 RAII 实现。例如,在 C + + 中没有与 synchronizedlock等价的直接内建,但是我们仍然可以使用它们。

在 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 + + 开发人员的角度来看)。

例如,您可以编写一个计数器对象(我将其作为练习) ,并通过声明其变量来使用它,就像使用上面的 lock 对象一样:

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. Why does C++ lack finally?

期末考试倒计时!

ー欧洲: 碧血长天(1980年电影)(对不起,我没有引号了,这里... : -)

在 C #/Java 中,finally子句用于在范围退出(通过 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 可以做很多很多..。