ChatGPT解决这个技术问题 Extra ChatGPT

资源获取即初始化 (RAII) 是什么意思?

资源获取即初始化 (RAII) 是什么意思?


t
the_mandrill

对于一个非常强大的概念来说,这是一个非常糟糕的名字,也许是 C++ 开发人员在切换到其他语言时错过的第一件事。尝试将这个概念重命名为 Scope-Bound Resource Management 有一些运动,尽管它似乎还没有流行起来。

当我们说“资源”时,我们不仅仅指内存——它可以是文件句柄、网络套接字、数据库句柄、GDI 对象……简而言之,我们的供应有限,因此我们需要能够控制它们的使用。 “范围绑定”方面意味着对象的生命周期绑定到变量的范围,因此当变量超出范围时,析构函数将释放资源。一个非常有用的特性是它可以提高异常安全性。例如,比较这个:

RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation();  // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks

与 RAII 一

class ManagedResourceHandle {
public:
   ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
   ~ManagedResourceHandle() {delete rawHandle; }
   ... // omitted operator*, etc
private:
   RawResourceHandle* rawHandle;
};

ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();

在后一种情况下,当抛出异常并解开堆栈时,局部变量将被销毁,从而确保我们的资源被清理并且不会泄漏。


@the_mandrill:我试过 ideone.com/1Jjzuc 这个程序。但是没有析构函数调用。 tomdalling.com/blog/software-design/... 说 C++ 保证将调用堆栈上对象的析构函数,即使抛出异常也是如此。那么,为什么析构函数没有在这里执行呢?我的资源是泄露了还是永远不会被释放或释放?
抛出异常,但您没有捕获它,因此应用程序终止。如果您使用 try { } catch () {} 进行包装,那么它会按预期工作:ideone.com/xm2GR9
不太确定 Scope-Bound 是否是此处的最佳名称选择,因为存储类说明符范围一起确定实体的存储持续时间。将其缩小到范围限制可能是一个有用的简化,但它不是 100% 精确
但是您如何解释原始名称中的RA is initialization?我对 RAII 的理解是,一旦超出范围,每个对象都有责任处理其删除。对我来说,它与 is initialization 不匹配,因为一切都与析构函数有关。我仍然对那个成语名称感到困惑。
“... C++ 开发人员错过的第一件事...” 这不是类似于 Java 中的 try-with-resources 吗?它似乎解决了同样的问题,与 Java 的解决方案相比,我认为 RAII 没有优势或劣势。
P
Péter Török

这是一个编程习语,它简要地表示你

将资源封装到一个类中(其构造函数通常 - 但不一定** - 获取资源,其析构函数总是释放它)

通过类的本地实例使用资源*

当对象超出范围时资源会自动释放

这保证了资源在使用时无论发生什么,它最终都会被释放(无论是由于正常返回、包含对象的破坏还是抛出异常)。

这是 C++ 中广泛使用的良好实践,因为除了作为一种安全的资源处理方式之外,它还使您的代码更加简洁,因为您不需要将错误处理代码与主要功能混合在一起。

* 更新:“local”可能表示局部变量或类的非静态成员变量。在后一种情况下,成员变量将使用其所有者对象进行初始化和销毁。

** Update2: 正如@sbi 指出的那样,资源(尽管通常在构造函数内部分配)也可以在外部分配并作为参数传入。


AFAIK,首字母缩略词并不意味着对象必须位于本地(堆栈)变量上。它可能是另一个对象的成员变量,所以当'持有'对象被销毁时,成员对象也被销毁,并且资源被释放。实际上,我认为首字母缩略词仅表示没有 open()/close() 方法来初始化和释放资源,只有构造函数和析构函数,因此资源的“持有”只是对象的生命周期,无论该生命周期是由上下文(堆栈)还是显式(动态分配)处理
实际上没有说必须在构造函数中获取资源。文件流、字符串和其他容器会这样做,但资源也可以传递给构造函数,就像智能指针通常的情况一样。由于您的答案是最受好评的答案,因此您可能需要解决此问题。
它不是首字母缩写词,它是缩写词。 IIRC 大多数人都将其发音为“ar ey ay ay”,因此它并不真正符合说 DARPA 之类的首字母缩略词的条件,发音为 DARPA 而不是拼写。另外,我会说 RAII 是一种范式,而不仅仅是一个成语。
@Peter Torok:我试过ideone.com/1Jjzuc这个程序。但是没有析构函数调用。 tomdalling.com/blog/software-design/… 表示 C++ 保证将调用堆栈上对象的析构函数,即使抛出异常也是如此。那么,为什么析构函数没有在这里执行呢?我的资源是泄露了还是永远不会被释放或释放?
在该示例中,未捕获异常,因此程序立即终止。如果您捕获到异常,则在展开堆栈时调用析构函数。
R
Ry-

“RAII”代表“Resource Acquisition is Initialization”,实际上是用词不当,因为它关注的不是资源获取(和对象的初始化),而是释放资源(通过对象的销毁) )。但 RAII 是我们得到的名字,它坚持下去。

从本质上讲,该惯用语的特点是在本地自动对象中封装资源(内存块、打开的文件、解锁的互斥锁、您的名字),并让该对象的析构函数在对象被销毁时释放资源它所属范围的结尾:

{
  raii obj(acquire_resource());
  // ...
} // obj's dtor will call release_resource()

当然,对象并不总是本地的、自动的对象。他们也可以是一个类的成员:

class something {
private:
  raii obj_;  // will live and die with instances of the class
  // ... 
};

如果此类对象管理内存,它们通常被称为“智能指针”。

这有很多变化。例如,在第一个代码片段中,如果有人想要复制 obj,会出现什么问题。最简单的方法是简单地禁止复制。 std::unique_ptr<> 是作为下一个 C++ 标准所采用的标准库一部分的智能指针。
另一个这样的智能指针 std::shared_ptr 具有资源的“共享所有权”(动态分配的对象)它拥有。也就是说,它可以自由复制,所有的副本都指向同一个对象。智能指针跟踪有多少副本引用同一个对象,并在最后一个被销毁时将其删除。
第三种变体是 std::auto_ptr 的特色,它实现了一种移动语义:一个对象是仅由一个指针拥有,并且尝试复制对象将导致(通过语法黑客)将对象的所有权转移到复制操作的目标。


std::auto_ptrstd::unique_ptr 的过时版本。 std::auto_ptr 在 C++98 中尽可能多地模拟移动语义,std::unique_ptr 使用 C++11 的新移动语义。创建新类是因为 C++11 的移动语义更加明确(需要 std::move,除了临时的),而它默认用于 std::auto_ptr 中非常量的任何副本。
@JiahaoCai:很多年前(在Usenet 上),Stroustrup 本人曾这样说过。
e
elmiomar

一个对象的生命周期是由它的作用域决定的。然而,有时我们需要,或者它是有用的,创建一个独立于创建它的范围的对象。在 C++ 中,运算符 new 用于创建这样的对象。而要销毁对象,可以使用运算符deletenew 运算符创建的对象是动态分配的,即分配在动态内存中(也称为heapfree store)。因此,由 new 创建的对象将继续存在,直到使用 delete 明确销毁它。

使用 newdelete 时可能出现的一些错误是:

泄漏的对象(或内存):使用new分配一个对象而忘记删除该对象。

过早删除(或悬空引用):持有另一个指向对象的指针,删除该对象,然后使用另一个指针。

双重删除:试图删除一个对象两次。

通常,范围变量是首选。但是,RAII 可以用作 newdelete 的替代方案,以使对象独立于其范围而存在。这种技术包括获取指向在堆上分配的对象的指针并将其放置在句柄/管理器对象中。后者有一个析构函数,负责销毁对象。这将保证该对象可供任何想要访问它的函数使用,并且该对象在 句柄对象 的生命周期结束时被销毁,而无需显式清理。

使用 RAII 的 C++ 标准库中的示例是 std::stringstd::vector

考虑这段代码:

void fn(const std::string& str)
{
    std::vector<char> vec;
    for (auto c : str)
        vec.push_back(c);
    // do something
}

当您创建一个向量并将元素推送到它时,您并不关心分配和释放这些元素。向量使用 new 为其在堆上的元素分配空间,并使用 delete 释放该空间。作为 vector 的用户,您不关心实现细节,并且会相信 vector 不会泄漏。在这种情况下,向量是其元素的 句柄对象

标准库中使用 RAII 的其他示例包括 std::shared_ptrstd::unique_ptrstd::lock_guard

这种技术的另一个名称是 SBRM,是 Scope-Bound Resource Management 的缩写。


“SBRM”对我来说更有意义。我提出这个问题是因为我认为我理解 RAII,但这个名字让我大吃一惊,听到它被描述为“范围绑定资源管理”让我立即意识到我确实理解了这个概念。
我不确定为什么没有将此标记为问题的答案。这是一个非常详尽且写得很好的答案,谢谢@elmiomar
D
Dennis

本书 C++ Programming with Design Patterns Revealed 将 RAII 描述为:

获取所有资源 使用资源 释放资源

在哪里

资源被实现为类,并且所有指针都有围绕它们的类包装器(使它们成为智能指针)。

资源通过调用它们的构造函数来获取,并通过调用它们的析构函数隐式释放(以获取的相反顺序)。


@Brandin 我编辑了我的帖子,以便读者将注意力集中在重要的内容上,而不是争论版权法的灰色地带什么构成合理使用。
M
Mohammad Moridi

RAII 类包含三个部分:

资源在析构函数中被放弃类的实例在堆栈中分配资源在构造函数中获取。这部分是可选的,但很常见。

RAII 代表“资源获取是初始化”。 RAII 的“资源获取”部分是您开始一些必须稍后结束的事情的地方,例如:

打开文件分配一些内存获取锁

“正在初始化”部分意味着获取发生在类的构造函数内部。

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/


D
Dmitry Pavlov

自编译器发明以来,手动内存管理是程序员一直在发明方法来避免的噩梦。带有垃圾收集器的编程语言使生活更轻松,但以性能为代价。在这篇文章 - Eliminating the Garbage Collector: The RAII Way 中,Toptal 工程师 Peter Goodspeed-Niklaus 向我们介绍了垃圾收集器的历史,并解释了所有权和借用的概念如何帮助消除垃圾收集器而不损害其安全保证。


O
Oleg Kokorin

RAII 概念只是一个 C 堆栈变量的想法。最简单的解释方式。


e
elexotics N

许多人认为 RAII 用词不当,但实际上它是这个成语的正确名称,只是没有很好地解释。

Wikipedia 详细解释了行为:资源获取即初始化 (RAII) 是一种编程习惯用法,用于几种面向对象、静态类型的编程语言中,用于描述特定语言的行为。在 RAII 中,持有资源是类不变量,并且与对象生命周期相关联:资源分配(或获取)在对象创建(特别是初始化)期间由构造函数完成,而资源释放(释放)在对象销毁期间完成(具体完成),由析构函数。换句话说,资源获取必须成功,初始化才能成功。因此,资源被保证在初始化完成和终结开始之间被持有(持有资源是类不变量),并且只在对象处于活动状态时被持有。因此,如果没有对象泄漏,就没有资源泄漏。

而现在的名字,它只是意味着“资源获取”的动作是一个初始化动作,应该是资源类对象的初始化/构造函数的一部分。换句话说,使用这个成语,使用资源意味着创建一个资源类来保存资源并在构造类对象时初始化资源。隐含地,它表明资源的释放应该在资源类析构函数中对称地发生。

这有什么用?您当然可以选择不使用此成语,但如果您想知道使用此成语会得到什么,请考虑

RAII 对于更大的 C++ 项目,在构造函数/析构函数对之外不包含对 new 或 delete(或 malloc/free)的单个调用是很常见的。或者根本没有,事实上。

你可以避免

exit: free_resouce() // 在退出函数之前清理资源

或使用 RAII lock,这样您就不会忘记解锁。


R
Rick

我已经多次回到这个问题并阅读了它,我认为投票最高的答案有点误导。

RAII 的关键是:

“这(主要)不是关于捕捉异常,它主要是关于管理资源的所有权。”

投票最高的答案夸大了异常安全,这让我感到困惑。

事实是:

您仍然需要编写 try catch 来处理异常(查看下面的 2 个代码示例),除了您不必担心在 catch 块中使用 RAII 为这些类释放资源。否则,您需要查找每个非 RAII 类的 API 来查找调用哪个函数,以便在 catch 块中释放获取的资源。 RAII 简单地保存了这些工作。与上面类似,使用 RAII 编码时,您只需编写更少的代码,无需调用释放资源函数。所有的清理工作都在析构函数中完成。

另外,请检查我在上面的评论中发现有用的这 2 个代码示例。

https://ideone.com/1Jjzuchttps://ideone.com/xm2GR9

PS One 可以与 python with .. as 语句进行比较,您还需要捕获可能发生在 with 块内的异常。


这些示例有些模糊,因为它们不是有效的 C++。 int *p = new int[-1]; 行不编译。