ChatGPT解决这个技术问题 Extra ChatGPT

理解术语和概念的含义 - RAII (Resource Acquisition is Initialization)

各位 C++ 开发人员能否给我们一个好的描述 RAII 是什么,为什么它很重要,以及它是否可能与其他语言有任何关系?

我知道一点。我相信它代表“资源获取就是初始化”。然而,这个名字与我(可能不正确)对 RAII 的理解不符:我的印象是 RAII 是一种在堆栈上初始化对象的方式,这样,当这些变量超出范围时,析构函数将自动被调用导致资源被清理。

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

你怎么能在堆栈上做一些东西来清理堆上的东西呢?另外,是否存在不能使用 RAII 的情况?您是否曾经发现自己希望进行垃圾收集?至少一个垃圾收集器可以用于某些对象,同时让其他对象得到管理?

谢谢。

UTSTTC?我喜欢!它比 RAII 直观得多。 RAII 的名字很糟糕,我怀疑任何 C++ 程序员都会对此提出异议。但改变并不容易。 ;)
以下是 Stroustrup 对此事的看法:groups.google.com/group/comp.lang.c++.moderated/msg/…
@sbi:无论如何,对您的评论 +1 只是为了历史研究。我相信拥有作者 (B. Stroustrup) 对概念名称 (RAII) 的观点很有趣,可以有自己的答案。
@paercebal:历史研究?现在你让我觉得自己很老了。 :( 那时我正在阅读整个线程,甚至不认为自己是 C++ 新手!
+1,我正要问同样的问题,很高兴我不是唯一一个理解这个概念但对这个名字没有意义的人。似乎它应该被称为 RAOI - Resource Acquisition On Initialization。

p
peterchen

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

RAII 告诉您该做什么:在构造函数中获取您的资源!我要补充:一种资源,一种构造函数。 UTSTTC 只是其中的一种应用,RAII 更多。

资源管理很烂。在这里,资源是使用后需要清理的任何东西。对跨平台项目的研究表明,大多数错误都与资源管理有关——在 Windows 上尤其严重(由于对象和分配器的类型很多)。

在 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;
    }
}

如果构造失败(有一个例外),则不会调用其他成员函数——甚至析构函数——也不会被调用。

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!";
    }
}

如果 original 的构造函数失败(因为无法打开 filename1)、duplex 的构造函数失败(因为无法打开 filename2)或写入文件,Logger 的构造函数将失败Logger 的构造函数体内失败。在任何这些情况下,Logger 的析构函数将不会被调用 - 因此我们不能依赖 Logger 的析构函数来释放文件。但是如果 original 被构造,它的析构函数将在 Logger 构造函数的清理过程中被调用。

RAII 简化了部分构建后的清理工作。

负面观点:

负分?所有问题都可以通过 RAII 和智能指针解决;-)

当您需要延迟采集时,RAII 有时会很笨拙,将聚合对象推送到堆上。
想象一下 Logger 需要一个 SetTargetFile(const char* target)。在这种情况下,仍需要成为 Logger 成员的句柄需要驻留在堆上(例如,在智能指针中,以适当地触发句柄的销毁。)

我从来没有真正希望垃圾收集。当我做 C# 时,我有时会感到一种幸福,我不需要在意,但更想念所有可以通过确定性破坏创造的酷玩具。 (使用 IDisposable 只是不会削减它。)

我有一个可能受益于 GC 的特别复杂的结构,其中“简单”的智能指针会导致对多个类的循环引用。我们通过仔细平衡强指针和弱指针来糊里糊涂,但任何时候我们想改变一些东西,我们都必须研究一个大的关系图。 GC 可能会更好,但是一些组件拥有应该尽快释放的资源。

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


+1 指出 GC 和 ASAP 不啮合。不经常受伤,但当它受伤时,诊断并不容易:/
特别是我在早期阅读中忽略的一句话。您说“RAII”是在告诉您,“在构造函数中获取资源”。这是有道理的,几乎是“RAII”的逐字释义。现在我做得更好了(如果可以的话,我会再次投票给你:)
GC 的一个主要优点是内存分配框架可以防止在没有“不安全”代码的情况下创建悬空引用(如果允许“不安全”代码,当然,框架无法阻止任何事情)。在处理共享的不可变对象(例如通常没有明确所有者且不需要清理的字符串)时,GC 通常也优于 RAII。不幸的是,更多的框架不寻求结合 GC 和 RAII,因为大多数应用程序将混合不可变对象(GC 最好)和需要清理的对象(RAII 最好)。
@supercat:我通常喜欢 GC - 但它仅适用于 GC“理解”的资源。例如,.NET GC 不知道 COM 对象的成本。当简单地在一个循环中创建和销毁它们时,它会很高兴地让应用程序在地址空间或虚拟内存方面陷入困境 - 无论是先出现的 - 甚至不考虑可能进行 GC。 --- 此外,即使在完美的 GC 环境中,我仍然怀念确定性破坏的力量:您可以将相同的模式应用于其他工件,例如在特定条件下显示 UI 元素。
@peterchen:我认为在许多与 OOP 相关的思想中缺少的一件事是对象所有权的概念。对于有资源的对象,显然需要跟踪所有权,但对于没有资源的可变对象,也经常需要跟踪所有权。通常,对象应该将其可变状态封装在对可能共享的不可变对象的引用中,或者封装在它们是其独占所有者的可变对象中。这种独占所有权并不一定意味着独占写入访问权限,但如果 Foo 拥有 Bar,并且 Boz 对其进行了变异,...
p
paercebal

那里有很好的答案,所以我只是添加一些忘记的东西。

0. RAII 是关于范围的

RAII 是关于两者:

在构造函数中获取资源(无论是什么资源),并在析构函数中取消获取它。在声明变量时执行构造函数,并在变量超出范围时自动执行析构函数。

其他人已经回答了,所以我不会详细说明。

1. 使用 Java 或 C# 编码时,您已经使用 RAII...

乔丹先生:什么!当我说,“妮可,给我拖鞋,给我睡帽,”那是散文吗?哲学大师:是的,先生。 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:互斥量获取在关键字(synchronizedlock)中完成,退出范围时将完成取消获取。

它的符号非常自然,即使对于从未听说过 RAII 的人来说也几乎不需要解释。

在这里,C++ 相对于 Java 和 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++ 开发人员的角度来看)。

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

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 子句在 C#/Java 中用于在范围退出(通过 return 或引发的异常)的情况下处理资源处置。

精明的规范读者会注意到 C++ 没有 finally 子句。这不是错误,因为 C++ 不需要它,因为 RAII 已经处理资源处理。 (相信我,编写 C++ 析构函数比编写正确的 Java finally 子句甚至 C# 的正确 Dispose 方法要容易得多)。

不过,有时,finally 子句会很酷。我们可以在 C++ 中做到这一点吗? Yes, we can! 再次使用 RAII。

结论:RAII 不仅仅是 C++ 中的哲学:它是 C++

雷伊?这是C++!!! —— C++ 开发者的愤怒评论,被一个不起眼的斯巴达国王和他的 300 位朋友无耻地抄袭

当您在 C++ 方面达到一定程度的经验时,您会开始考虑 RAII,即构造函数和析构函数的自动执行。

您开始考虑 范围{} 字符成为代码中最重要的字符。

几乎所有东西都适合 RAII:异常安全、互斥体、数据库连接、数据库请求、服务器连接、时钟、操作系统句柄等,最后但并非最不重要的是内存。

数据库部分也不容小觑,因为如果你愿意付出代价,你甚至可以写成“事务性编程”的风格,执行一行又一行的代码,直到最后决定,如果您希望提交所有更改,或者,如果不可能,将所有更改还原(只要每行至少满足强异常保证)。 (有关事务性编程,请参阅此 Herb's Sutter article 的第二部分)。

就像一个谜题,一切都合适。

RAII 是 C++ 的重要组成部分,没有它,C++ 就不可能是 C++。

这就解释了为什么有经验的 C++ 开发人员如此迷恋 RAII,以及为什么 RAII 是他们在尝试另一种语言时首先搜索的东西。

它还解释了为什么垃圾收集器虽然本身就是一项了不起的技术,但从 C++ 开发人员的角度来看并没有那么令人印象深刻:

RAII 已经处理了 GC 处理的大部分案件

GC 比 RAII 更好地处理纯托管对象的循环引用(通过智能使用弱指针来缓解)

仍然 GC 仅限于内存,而 RAII 可以处理任何类型的资源。

如上所述,RAII 可以做的很多很多……


一位 Java 粉丝:我想说 GC 比 RAII 更有用,因为它可以处理所有内存并将您从许多潜在的错误中解放出来。使用 GC,您可以创建循环引用、返回和存储引用,并且很难出错(存储对所谓短命对象的引用会延长其生存时间,这是一种内存泄漏,但这是唯一的问题) .使用 GC 处理资源不起作用,但应用程序中的大多数资源都有一个微不足道的生命周期,剩下的少数资源没什么大不了的。我希望我们可以同时拥有 GC 和 RAII,但这似乎是不可能的。
s
sharptooth

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

SmartPointer pointer( new ObjectClass() );

当智能指针超出范围时,指针类的析构函数会删除连接的对象。指针是堆栈分配的,对象是堆分配的。

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


所以它应该被称为UCSTMR :)
再想一想,我认为 UDSTMR 更合适。给出了语言 (C++),因此首字母缩略词中不需要字母“C”。 UDSTMR 代表使用析构函数语义来管理资源。
M
MSalters

我想把它比以前的回答更强烈一些。

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


很好的答案。 “对象的初始化”意味着“构造函数”,是吗?
@Charlie:是的,尤其是在这种情况下。
i
iain

我同意cpitis。但想补充一点,资源可以是任何东西,而不仅仅是内存。资源可以是文件、临界区、线程或数据库连接。

之所以称为资源获取即初始化,是因为在构造控制资源的对象时获取资源,如果构造函数失败(即由于异常),则不获取资源。然后,一旦对象超出范围,资源就会被释放。 c++保证栈上所有构造成功的对象都会被销毁(这包括基类和成员的构造函数,即使超类构造函数失败)。

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


太好了,感谢您解释名称背后的理由。据我了解,您可能会将 RAII 解释为“永远不要通过(基于构造函数的)初始化之外的任何其他机制获取任何资源”。是的?
是的,这是我的政策,但是我对编写自己的 RAII 类非常谨慎,因为它们必须是异常安全的。当我编写它们时,我尝试通过重用专家编写的其他 RAII 类来确保异常安全。
我没有发现它们很难写。如果您的课程足够小,那么它们一点也不难。
M
Mark Ransom

垃圾收集的问题在于您失去了对 RAII 至关重要的确定性破坏。一旦变量超出范围,则由垃圾收集器决定何时回收该对象。对象持有的资源将继续持有,直到调用析构函数。


问题不仅在于决定论。真正的问题是终结器(java 命名)妨碍了 GC。 GC 是高效的,因为它不会召回死对象,而是将它们忽略掉。 GC 必须以不同的方式跟踪带有终结器的对象,以保证它们被调用
除了在 java/c# 中,您可能会在 finally 块中而不是在终结器中进行清理。
C
Cătălin Pitiș

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

与垃圾收集语言/技术(例如 Java、.NET)相比,C++ 允许完全控制对象的生命周期。对于堆栈分配的对象,您将知道何时调用对象的析构函数(当执行超出范围时),这在垃圾收集的情况下不受真正控制。即使在 C++ 中使用智能指针(例如 boost::shared_ptr),您也会知道当没有对指向对象的引用时,将调用该对象的析构函数。


J
Jeremiah Willcock

你怎么能在堆栈上做一些东西来清理堆上的东西呢?

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_buffer 的实例存在时,它必须具有大小,并且它将分配必要的内存。当它超出范围时,它的析构函数被调用。这对于诸如同步对象之类的东西非常有用。考虑

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.

另外,是否存在不能使用 RAII 的情况?

不,不是。

您是否曾经发现自己希望进行垃圾收集?至少一个垃圾收集器可以用于某些对象,同时让其他对象得到管理?

绝不。垃圾收集只解决了动态资源管理的一小部分。


我很少使用 Java 和 C#,所以我从来没有错过它,但是当我不得不使用资源管理时,GC 肯定会限制我的风格,因为我不能使用 RAII。
我经常使用 C# 并且 100% 同意你的观点。事实上,我认为非确定性 GC 是一种语言的责任。
E
E Dominique

这里已经有很多很好的答案,但我想补充一下:对 RAII 的简单解释是,在 C++ 中,分配在堆栈上的对象在超出范围时被销毁。这意味着,将调用对象析构函数并可以进行所有必要的清理。这意味着,如果创建的对象没有“new”,则不需要“delete”。这也是“智能指针”背后的理念——它们驻留在堆栈上,并且本质上包装了一个基于堆的对象。


不,他们没有。但是你有充分的理由在堆上创建一个智能指针吗?顺便说一句,智能指针只是 RAII 有用的一个例子。
也许我对“堆栈”与“堆”的使用有点草率——“堆栈”上的一个对象是指任何本地对象。它自然可以是对象的一部分,例如在堆上。通过“在堆上创建一个智能指针”,我的意思是在智能指针本身上使用 new/delete。
t
techcraver

RAII 是 Resource Acquisition Is Initialization 的首字母缩写词。

这种技术对于 C++ 非常独特,因为它们支持构造函数和析构函数,并且几乎自动支持与传入的参数匹配的构造函数,或者在最坏的情况下调用默认构造函数,如果显式提供则调用析构函数,否则调用默认构造函数如果您没有为 C++ 类显式编写析构函数,则调用由 C++ 编译器添加的。这只发生在自动管理的 C++ 对象上——这意味着不使用空闲存储(使用 new,new[]/delete,delete[] C++ 运算符分配/释放的内存)。

RAII 技术利用这种自动管理的对象特性来处理在堆/空闲存储上创建的对象,方法是使用 new/new[] 显式请求更多内存,应该通过调用 delete/delete[] 显式销毁这些对象.自动管理对象的类将包装在堆/空闲存储内存上创建的另一个对象。因此,当运行自动管理对象的构造函数时,将在堆/空闲存储内存上创建包装对象,并且当自动管理对象的句柄超出范围时,自动调用该自动管理对象的析构函数,其中包装对象使用 delete 销毁对象。使用 OOP 概念,如果您将此类对象包装在私有范围内的另一个类中,您将无法访问包装的类成员和方法,这就是智能指针(又名句柄类)的设计目的。这些智能指针将包装的对象作为类型化对象暴露给外部世界,并允许调用暴露的内存对象组成的任何成员/方法。请注意,智能指针根据不同的需求有不同的风格。您应该参考 Andrei Alexandrescu 的 Modern C++ Programming 或 boost 库的 (www.boostorg) shared_ptr.hpp implementation/documentation 以了解更多信息。希望这可以帮助您了解 RAII。