114

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

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

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

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

谢谢。

4

10 回答 10

136

那么为什么不称为“使用堆栈触发清理”(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调用' 的析构函数- 所以我们不能依赖' 的析构函数来释放文件。但是如果被构造,它的析构函数将在构造函数的清理过程中被调用。LoggeroriginalLogger

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


负面观点:

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

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

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

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


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

于 2009-04-03T12:51:29.520 回答
42

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

0. RAII 是关于范围的

RAII 是关于两者:

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

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

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

乔丹先生:什么!当我说,“妮可,给我拖鞋,给我睡帽,”那是散文吗?

哲学大师:是的,先生。

MONSIEUR JOURDAIN:四十多年来,我一直在说散文,但对此一无所知,我非常感谢你教会了我这一点。

——莫里哀:中产阶级绅士,第 2 幕,第 4 场

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

void foo()
{
   // etc.

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

   // etc.
}

...已经在使用RAII:互斥量获取在关键字(synchronizedlock)中完成,退出范围时将完成取消获取。

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

在这里,C++ 相对于 Java 和 C# 的优势是可以使用 RAII 制作任何东西。synchronized例如,在 C++ 中没有等价于nor的直接内置lock函数,但我们仍然可以拥有它们。

在 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

[呼喊] 这是最后的倒计时!

— 欧洲:最后的倒计时(对不起,我没有报价,在这里... :-)

finally子句在 C#/Java 中用于在范围退出(通过return异常或抛出异常)的情况下处理资源处置。

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

不过,有时,一个finally条款会很酷。我们可以在 C++ 中做到这一点吗?我们可以!再次使用 RAII。

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

雷伊?这是C++!!!

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

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

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

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

数据库部分是不可忽视的,因为如果你愿意付出代价,你甚至可以用“事务性编程”风格编写,执行一行又一行的代码,直到最后决定是否要提交所有更改,或者,如果不可能,则将所有更改还原(只要每行至少满足强异常保证)。(有关事务性编程,请参阅这篇Herb 的 Sutter 文章的第二部分)。

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

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

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

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

  • RAII 已经处理了 GC 处理的大部分案件
  • GC 比 RAII 更好地处理纯托管对象的循环引用(通过智能使用弱指针来缓解)
  • 仍然 GC 仅限于内存,而 RAII 可以处理任何类型的资源。
  • 如上所述,RAII 可以做的很多很多……
于 2011-01-19T21:24:40.777 回答
10

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

SmartPointer pointer( new ObjectClass() );

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

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

于 2009-04-03T05:28:39.507 回答
9

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

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

于 2009-04-03T08:50:29.100 回答
8

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

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

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

于 2009-04-03T07:45:31.440 回答
7

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

于 2009-04-03T05:34:56.543 回答
4

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

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

于 2009-04-03T06:08:25.883 回答
3

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

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 的情况?

不,不是。

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

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

于 2009-04-03T15:21:38.193 回答
2

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

于 2009-04-03T11:04:53.910 回答
1

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。您应该参考 Andrei Alexandrescu 的 Modern C++ Programming 或 boost 库的 (www.boostorg) shared_ptr.hpp implementation/documentation 以了解更多信息。希望这可以帮助您了解 RAII。您应该参考 Andrei Alexandrescu 的 Modern C++ Programming 或 boost 库的 (www.boostorg) shared_ptr.hpp implementation/documentation 以了解更多信息。希望这可以帮助您了解 RAII。

于 2009-04-03T10:03:25.133 回答