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

Posted

技术标签:

【中文标题】理解术语和概念的含义 - RAII (Resource Acquisition is Initialization)【英文标题】:Understanding the meaning of the term and the concept - RAII (Resource Acquisition is Initialization) 【发布时间】:2010-10-17 07:01:07 【问题描述】:

能否请 C++ 开发人员向我们详细描述一下 RAII 是什么,为什么它很重要,以及它是否可能与其他语言有任何关联?

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

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

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

谢谢。

【问题讨论】:

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

那为什么不叫“使用堆栈触发清理”(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!";
    

Logger的构造函数会失败如果original的构造函数失败(因为filename1无法打开),duplex的构造函数失败(因为filename2无法打开),或者写到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 的一个主要优点是内存分配框架可以防止在没有“不安全”代码的情况下创建悬空引用(如果允许“不安全”代码,当然,框架可以' t 阻止任何事情)。在处理共享的 immutable 对象(如通常没有明确所有者且不需要清理的字符串)时,GC 通常也优于 RAII。不幸的是,更多的框架不寻求结合 GC 和 RAII,因为大多数应用程序将混合不可变对象(GC 最好)和需要清理的对象(RAII 最好)。 @supercat:我通常喜欢 GC - 但它仅适用于 GC“理解”的资源。例如。 .NET GC 不知道 COM 对象的成本。当简单地在一个循环中创建和销毁它们时,它会很高兴地让应用程序在地址空间或虚拟内存方面陷入困境 - 无论是先出现的 - 甚至不考虑可能进行 GC。 --- 此外,即使在完美的 GC 环境中,我仍然怀念确定性破坏的力量:您可以将相同的模式应用于其他工件,例如在特定条件下显示 UI 元素。 @peterchen:我认为在许多与 OOP 相关的思想中缺少的一件事是对象所有权的概念。对于有资源的对象,显然需要跟踪所有权,但对于没有资源的可变对象,也经常需要跟踪所有权。一般来说,对象应该将它们的可变状态封装在对可能共享的不可变对象的引用中,或者封装在它们是其独占所有者的可变对象中。这种独占所有权并不一定意味着独占写访问,但如果Foo 拥有Bar,并且Boz 对其进行了变异,...【参考方案2】:

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

0。 RAII 是关于范围

RAII 是关于两者的:

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

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

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

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

哲学大师:是的,先生。

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

[大喊]这是决赛倒计时!

— 欧洲:最后的倒计时(抱歉,我没有引号,这里...:-)

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

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

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

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

RAII?这是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,但这似乎是不可能的。【参考方案3】:

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

SmartPointer pointer( new ObjectClass() );

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

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

【讨论】:

所以应该叫UCDSTMR :) 再想一想,我认为UDSTMR 更合适。给出了语言 (C++),因此首字母缩略词中不需要字母“C”。 UDSTMR 代表 Using Destructor Semantics To Manage Resources。【参考方案4】:

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

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

【讨论】:

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

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

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

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

【讨论】:

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

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

【讨论】:

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

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

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

【讨论】:

【参考方案8】:

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

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 在语言中是一种负担。【参考方案9】:

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

【讨论】:

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

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

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

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

【讨论】:

以上是关于理解术语和概念的含义 - RAII (Resource Acquisition is Initialization)的主要内容,如果未能解决你的问题,请参考以下文章

彻底征服 Spring AOP 之 理论篇

[转]彻底征服 Spring AOP 之 理论篇

如何使用 RAII 对套接字进行建模

9,000+ 字,彻底征服 Spring AOP!

Haskell 术语:类型与数据类型的含义,它们是同义词吗?

单一与共享所有权的含义