了解术语和概念的含义-RAII(资源获取是初始化)


110

C ++开发人员能否请我们很好地描述RAII是什么,为什么它很重要以及它与其他语言是否有关联?

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

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

以及如何在堆栈上生成将清除堆中内容的内容?另外,在某些情况下您不能使用RAII?您是否曾经希望进行垃圾收集?至少您可以对某些对象使用垃圾收集器,而对其他对象进行管理?

谢谢。


27
UTSTTC?我喜欢!比RAII直观得多。RAII 名字不好用,我怀疑任何C ++程序员都会对此表示怀疑。但这并不容易改变。;)
杰夫

10
这是Stroustrup对此事的看法:groups.google.com/group/comp.lang.c
++.moderated/msg/…

3
@sbi:无论如何,您的评论+1只是为了进行历史研究。我相信,对于一个概念的名称(RAII),作者(B. Stroustrup)的观点很有趣,可以有自己的答案。
paercebal

1
@paercebal:历史研究?现在,您让我感到很老。:(那时我正在阅读整个线程,甚至都不认为自己是C ++新手!
2011年

3
+1,我将要问同样的问题,很高兴我不是唯一一个理解概念但不了解名称的人。似乎它应该被称为RAOI-初始化时的资源获取。
劳伦特

Answers:


132

那么为什么不称之为“使用堆栈触发清除”(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有时很笨拙,将聚合的对象推到堆上。
想象一下,记录仪需要一个SetTargetFile(const char* target)。在这种情况下,仍然需要是的成员的句柄Logger需要驻留在堆上(例如,在智能指针中),以适当触发该句柄的破坏。

我从未真正希望进行垃圾收集。当我做C#时,有时我会感到幸福,我只是不需要在乎,但是我更想念所有通过确定性破坏而创造的酷玩具。(IDisposable仅使用它并不能削减它。)

我有一个特别复杂的结构,该结构可能会从GC中受益,其中“简单”的智能指针将导致对多个类的循环引用。我们通过仔细平衡强指针和弱指针来弄混整个过程,但是每当我们要更改某些内容时,都必须研究一个大的关系图。GC可能更好,但是某些组件拥有应尽快发布的资源。


关于FileHandle示例的注释:并非完整示例,仅是示例-但事实证明是不正确的。感谢Johannes Schaub指出并感谢FredOverflow将其转变为正确的C ++ 0x解决方案。随着时间的流逝,我已经解决了这里记录的方法。


1
+1用于指出GC和ASAP不啮合。不会经常受到伤害,但是很难做到:/
Matthieu M.

10
特别是一句话,我在先前的阅读中忽略了这句话。您说“ RAII”告诉您“在构造函数中获取资源”。这是有道理的,几乎是“ RAII”的逐字释义。现在我变得更好了(如果可以的话,我会再次投票给您):
查理·弗劳德

2
GC的一个主要优点是,内存分配框架可以防止在没有“不安全”代码的情况下创建悬挂引用(如果允许“不安全”代码,则该框架当然不能阻止任何事情)。当处理共享的不可变对象(例如字符串)时,GC通常也比RAII更好,这些对象通常没有明确的所有者并且不需要清理。不幸的是,更多的框架没有寻求将GC和RAII结合起来,因为大多数应用程序将混合使用不可变对象(GC最好)和需要清除的对象(RAII最好)。
2012年

@supercat:我通常喜欢GC-但它仅适用于GC“了解”的资源。例如,.NET GC不知道COM对象的成本。当简单地在一个循环中创建和销毁它们时,它会很乐意让应用程序在地址空间或虚拟内存方面陷入困境(无论先发生什么),甚至都不会考虑进行GC。---而且,即使在完美的GC环境中,我仍然想念确定性破坏的力量:您可以将相同的模式应用于其他技巧,例如在特定条件下显示UI元素。
peterchen

@peterchen:我认为很多与OOP相关的思想中缺少的一件事是对象所有权的概念。显然,拥有资源的对象经常需要拥有所有权,但是对于没有资源的可变对象,经常也必须保持所有权。通常,对象应封装其可变状态,要么引用可能共享的不可变对象,要么将其封装为唯一所有者的可变对象。这样的排他性所有权不一定意味着排他性写访问权,而是如果Foo拥有Bar并对其进行了Boz突变,...
supercat 2012年

42

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

0. RAII与范围有关

RAII涉及以下两个方面:

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

其他人已经对此做出了回答,因此我不再赘述。

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

MONSIEUR JOURDAIN:什么!当我说“妮可,给我带来拖鞋,给我睡帽”时,是散文吗?

哲学大师:是的,先生。

MONSIEUR JOURDAIN:四十多年来,我一直在不了解散文的情况下讲散文,而且我很乐意告诉您这一点。

—莫里哀:中产阶级绅士,第二幕,第四幕

正如Jourdain先生对散文所做的那样,C#甚至Java人士已经在使用RAII,但是它们以隐藏的方式使用。例如,以下Java代码(用替换synchronized为C#,以相同的方式编写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进行任何操作。例如,有没有直接内建等效的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

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

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

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

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

有时候,一个finally子句仍然很酷。我们可以用C ++做到吗?我们可以!再次使用RAII。

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

RAII?这是C ++ !!!

— C ++开发人员的愤怒评论,由晦涩难懂的斯巴达国王和他的300个朋友无耻复制

当你到达的用C的经验一定程度++,你在以下方面开始思考RAII,在以下方面自动执行construtors和析构函数

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

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

数据库部分不可忽略,因为如果您接受支付,甚至可以以“ 事务性编程 ”风格编写代码,执行几行代码,直到最终确定是否要提交所有更改为止,或者,如果不可能的话,将所有更改恢复原状(只要每行至少满足“强异常保证”)。(请参阅此Herb Sutter文章的第二部分有关事务性编程,)。

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

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

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

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

  • RAII已经处理了GC处理的大多数案件
  • 与纯托管对象上的循环引用相比,GC的处理要比RAII更好(通过巧妙使用弱指针来缓解)
  • GC仍然限于内存,而RAII可以处理任何类型的资源。
  • 如上所述,RAII可以做的更多,更多...

Java迷:我想说GC比RAII有用,因为它可以处理所有内存并使您摆脱许多潜在的错误。使用GC,您可以创建循环引用,返回并存储引用,并且很难将其弄错(将引用存储到所谓的短期对象会延长其生存时间,这是一种内存泄漏,但这是唯一的问题) 。使用GC处理资源不起作用,但是应用程序中的大多数资源的生命周期都很短,而剩余的资源并不多。我希望我们可以同时拥有GC和RAII,但这似乎是不可能的。
maaartinus

16

1
其中一些与我的问题很吻合,但是搜索没有找到它们,在输入新问题后也没有出现“相关问题”列表。感谢您的链接。
查理·弗劳斯

1
@Charlie:搜索的构建在某些方面非常薄弱。使用标记语法(“ [topic]”)非常有帮助,许多人使用google ...
dmckee ---前主持人小猫

10

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

SmartPointer pointer( new ObjectClass() );

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

在某些情况下,RAII无法提供帮助。例如,如果您使用引用计数智能指针(例如boost :: shared_ptr)并创建具有循环的类似图形的结构,则可能会面临内存泄漏的风险,因为循环中的对象会阻止彼此释放。垃圾收集将对此有所帮助。


2
所以它应该被称为UCDSTMR :)
Daniel Daranas 09/09/16

再次考虑,我认为UDSTMR更合适。语言(C ++)已给出,因此首字母缩写中不需要字母“ C”。UDSTMR代表使用析构语义来管理资源。
Daniel Daranas

9

我想把它比以前的答复强一些。

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


好答案。“对象的初始化”的意思是“构造函数”,是吗?
查理·弗劳斯2010年

@查理:是的,尤其是在这种情况下。
MSalters 2010年

8

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

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

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


太好了,谢谢您解释该名称背后的原理。据我了解,您可能将RAII解释为:“除了(基于构造函数的)初始化之外,永远不要通过任何其他机制获取任何资源”。是?
查理·弗劳斯

是的,这是我的政策,但是我非常谨慎地编写自己的RAII类,因为它们必须是异常安全的。当我写它们时,我会尝试通过重用专家编写的其他RAII类来确保异常安全。
09年

我发现它们很难写。如果您的班级足够小,那么他们一点也不难。
罗布K

7

垃圾回收的问题在于您丢失了对RAII至关重要的确定性破坏。一旦变量超出范围,就取决于垃圾回收器何时回收对象。对象所保留的资源将继续保留,直到调用析构函数为止。


4
问题不仅仅在于确定性。真正的问题是终结器(java命名)妨碍了GC。GC是有效的,因为它不会调用死对象,而是将它们忽略掉。地方选区必须跟踪与终结的对象以不同的方式来保证他们被称为
dribeas大卫-罗德里格斯

1
除了在Java / C#中,您可能会在finally块中而不是在终结器中进行清理。
jk。

4

RAII来自“资源分配就是初始化”。基本上,这意味着当构造函数完成执行时,已构造的对象已完全初始化并可以使用。这也意味着析构函数将释放对象拥有的任何资源(例如,内存,OS资源)。

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


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?

不,不是。

您是否曾经希望进行垃圾收集?至少您可以对某些对象使用垃圾收集器,而对其他对象进行管理?

决不。垃圾回收只能解决动态资源管理的很小一部分。


我很少使用Java和C#,所以我从来没有错过它,但是当我不得不使用资源时,GC肯定会限制我的风格,因为我不能使用RAII。
罗布K

1
我经常使用C#,并100%同意。实际上,我认为非确定性GC是一种语言上的责任。
Nemanja Trifunovic

2

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


1
不,他们没有。但是,您是否有充分的理由在堆上创建智能指针?顺便说一句,智能指针只是RAII有用的一个例子。
E多米尼克2009年

1
也许我对“堆栈”和“堆”的使用有些草率-“堆栈”上的对象是任何本地对象。它自然可以是对象的一部分,例如在堆上。通过“在堆上创建智能指针”,我的意思是对智能指针本身使用new / delete。
E Dominique

1

RAII是“资源获取是初始化”的首字母缩写。

这种技术对于C ++而言非常独特,因为它们同时支持构造函数和析构函数,并且几乎自动支持与传入的参数匹配的构造函数,或者在最坏的情况下,默认提供的构造函数称为&析构函数(如果显式提供,则称为默认析构函数),否则默认为如果您没有为C ++类显式编写析构函数,则将调用C ++编译器添加的函数。这种情况仅发生在自动管理的C ++对象上,即不使用免费存储(使用new,new [] / delete,delete [] C ++运算符分配/取消分配的内存)。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.