关于c ++:理解术语和概念的含义 – RAII(资源获取是初始化)

Understanding the meaning of the term and the concept - RAII (Resource Acquisition is Initialization)

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

我知道一点。我相信它代表着"资源获取就是初始化"。然而,这个名称并不违背我对raii的理解(可能不正确):我认为raii是一种初始化堆栈上对象的方法,当这些变量超出范围时,将自动调用析构函数,从而清理资源。

那么,为什么不称之为"使用堆栈触发清理"(utsttc:)。你怎么从那里到"raii"?

你怎么能在堆上做一些东西,来清理堆上的东西呢?还有,你不能使用RAII的情况吗?你有没有想过要收集垃圾?至少可以为一些对象使用垃圾收集器,同时允许管理其他对象?

谢谢。


So why isn't that called"using the stack to trigger cleanup" (UTSTTC:)?

RAII告诉你该怎么做:在一个构造函数中获取你的资源!我要补充:一个资源,一个构造函数。UTSTTC只是其中的一个应用,RAII更是如此。

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

在C++中,由于异常和(C++样式)模板的组合,资源管理尤其复杂。要查看引擎盖下的信息,请参阅gotw8)。

C++保证,只要构造函数成功,就调用析构函数。基于此,RAII可以解决普通程序员甚至不知道的许多棘手问题。除了"我的局部变量将在每次返回时被销毁"之外,这里还有几个例子。

让我们从使用RAII的过于简单的FileHandle类开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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避免使用处于无效状态的对象。在我们使用这个物体之前,它已经让我们的生活更容易了。

现在,让我们来看看临时对象:

1
2
3
4
5
6
void CopyFileData(FileHandle source, FileHandle dest);

void Foo()
{
    CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}

有三个错误情况需要处理:不能打开文件,只能打开一个文件,两个文件都可以打开,但复制文件失败。在非RAII实现中,Foo必须明确处理这三个案例。

RAII释放被获取的资源,即使在一个语句中获取了多个资源。

现在,让我们聚合一些对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
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有时会很笨拙,将聚合的对象推送到堆中。假设记录器需要一个SetTargetFile(const char* target)。在这种情况下,仍然需要是Logger成员的句柄需要驻留在堆中(例如,在智能指针中,以适当触发句柄的销毁)。

我从来没有真正希望收集垃圾。当我这样做的时候,我有时会感到一种幸福的时刻,我不需要关心,但我更怀念通过确定性破坏所能创造出的所有酷玩具。(使用IDisposable并不能切断它。)

我有一个特别复杂的结构,它可能从GC中受益,"简单"的智能指针将导致多个类上的循环引用。我们通过仔细平衡强弱指针来应付,但是每当我们想要改变什么,我们就必须研究一个大的关系图。GC可能更好,但一些组件保留了应该尽快发布的资源。

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


有很好的答案,所以我只是添加了一些被遗忘的东西。好的。0。RAII是关于范围的

RAII涉及两个方面:好的。

  • 获取构造器中的资源(不管是什么资源),然后在析构函数中取消获取。
  • 在声明变量时执行构造函数,在变量超出范围时自动执行析构函数。
  • 其他人已经回答了这个问题,所以我不会详细说明。好的。1。当用Java或C语言编码时,你已经使用了RAII…

    MONSIEUR JOURDAIN: What! When I say,"Nicole, bring me my slippers,
    and give me my nightcap," that's prose?

    Ok.

    PHILOSOPHY MASTER: Yes, Sir.

    Ok.

    MONSIEUR JOURDAIN: For more than forty years I have been speaking prose without knowing anything about it, and I am much obliged to you for having taught me that.

    Ok.

    — Molière: The Middle Class Gentleman, Act 2, Scene 4

    Ok.

    正如Monsieur Jourdain对散文所做的那样,C·甚至Java人已经使用了RAII,但是隐藏起来了。例如,下面的Java代码(用EDCOX1(0)代替EDCOX1,1),用C语言编写同样的方法:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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++中,没有直接的Endox1×0Ω和EDCOX1 1Ω的等效构建,但我们仍然可以拥有它们。好的。

    在C++中,它将被写成:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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++宏):好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void foo()
    {
       // etc.

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

       // etc.
    }

    2。RAII有其他用途

    WHITE RABBIT: [singing] I'm late / I'm late / For a very important date. / No time to say"Hello." / Goodbye. / I'm late, I'm late, I'm late.

    Ok.

    — Alice in Wonderland (Disney version, 1951)

    Ok.

    您知道何时调用构造函数(在对象声明中),也知道何时调用其相应的析构函数(在作用域的出口处),所以只需一行代码就可以编写几乎不可思议的代码。欢迎来到C++仙境(至少从C++开发者的角度来看)。好的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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方式:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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
    }

    三。为什么C++缺少EDCOX1?6?< Buff行情>

    [喊叫]这是最后的倒计时!好的。

    -欧洲:最后的倒计时(对不起,我没有报价,这里……-)好的。< /块引用>

    EcOX1的0Ω子句用于C/J/Java处理范围内的资源处置(通过EDCOX1、1或抛出异常)。好的。

    精明的规范读者会注意到C++没有最终子句。这不是一个错误,因为C++不需要它,因为RAII已经处理了资源处理。(相信我,编写C++析构函数比写入正确的Java最终子句更容易,甚至C语言的正确处理方法)。好的。

    不过,有时,finally子句会很酷。我们能用C++做吗?是的,我们可以!再次使用RAII。好的。结论:在C++中,RAII不仅仅是一种哲学,它是C++。

    RAII? THIS IS C++!!!

    Ok.

    — C++ developer's outraged comment, shamelessly copied by an obscure Sparta king and his 300 friends

    Ok.

    当你在C++中达到某种程度的经验时,你就开始考虑RAII,在编译器和析构函数的自动执行方面。好的。

    您开始从范围的角度考虑问题,{}字符成为代码中最重要的字符之一。好的。

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

    数据库部分不可忽略,因为如果您接受支付价格,您甚至可以用"事务性编程"的方式编写,执行行和代码行,直到最后决定是否要提交所有更改,或者如果不可能,将所有更改还原回来(只要每行至少满足强异常n保证)。(有关事务性编程,请参阅本Herb的Sutter文章的第二部分)。好的。

    就像拼图一样,一切都很合适。好的。

    RAII是C++的一部分,没有C++,C++就不能成为C++。好的。

    这解释了为什么有经验的C++开发者如此热衷于RAII,以及为什么RAII是他们在尝试另一种语言时搜索的第一件事。好的。

    它解释了为什么垃圾收集器本身是一个巨大的技术,但从C++开发者的观点来看,它并不是那么令人印象深刻:好的。

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

    好啊。


    请参阅:

    除了C++之外,其他语言的程序员是否使用、了解或理解RAII?

    C++中的RAII和智能指针

    C++支持"最后"块吗?(我一直听到的这个"raii"是什么?)

    RAII与例外

    等。。


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

    1
    SmartPointer pointer( new ObjectClass() );

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

    有些情况下,RAII不起作用。例如,如果使用引用计数智能指针(如boost::shared_ptr),并创建一个具有循环的类似图的结构,则可能面临内存泄漏的风险,因为循环中的对象将阻止彼此释放。垃圾收集将有助于防止这种情况。


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

    资源获取是初始化,是指所有获取的资源都应该在对象初始化的上下文中获取。这禁止"裸"获取资源。其原理是C++中的清理工作在对象基础上,而不是函数调用基础上。因此,所有清理都应该由对象来完成,而不是由函数调用来完成。从这个意义上讲,C++是面向对象的,例如Java。Java清理是基于EDCOX1的0个子句中的函数调用。


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

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

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


    垃圾收集的问题是,您会丢失对raii至关重要的确定性破坏。一旦一个变量超出范围,当对象被回收时,它就由垃圾收集器决定。对象持有的资源将继续保持,直到调用析构函数。


    RAII来自资源分配初始化。基本上,它意味着当一个构造函数完成执行时,被构造的对象被完全初始化并准备好使用。它还意味着析构函数将释放对象拥有的任何资源(例如内存、操作系统资源)。

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


    And how can you make something on the stack that will cause the cleanup of something that lives on the heap?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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_缓冲区的实例出现时,它必须有一个大小,并且它将分配必要的内存。当它超出范围时,调用它的析构函数。这对于像同步对象这样的事情非常有用。考虑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    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.

    Also, are there cases where you can't use RAII?

    不,不是真的。

    Do you ever find yourself wishing for garbage collection? At least a garbage collector you could use for some objects while letting others be managed?

    从未。垃圾收集只解决了动态资源管理中非常小的一个子集。


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


    RAII是资源获取初始化的缩写。

    此技术对于C++非常独特,因为它们支持构造函数和析构函数;几乎自动地构造与正在传入的参数匹配的构造函数或最坏的情况下,如果提供了显式性,则调用默认构造函数和析构函数,否则调用默认构造函数。如果没有为C++类显式写析构函数,则调用C++编译器。这种情况只发生在自动管理的C++对象上,也就是说,不使用空闲存储(使用新的、新的[]/DELATE、Dele][C++运算符)分配/释放的内存。

    RAII技术利用此自动管理的对象功能,通过使用new/new[]明确请求更多内存来处理堆/空闲存储上创建的对象,该内存应通过调用delete/delete[]明确销毁。自动管理对象的类将包装在堆/可用存储内存上创建的另一个对象。因此,当自动托管对象的构造函数运行时,将在堆/空闲存储内存中创建包装的对象;当自动托管对象的句柄超出范围时,将自动调用该自动托管对象的析构函数,在该函数中使用delete销毁包装的对象。对于OOP概念,如果将这些对象包装在私有范围内的另一个类中,则无法访问包装的类成员和方法,这就是智能指针(也称为句柄类)设计的原因。这些智能指针通过允许调用公开内存对象所组成的任何成员/方法,将包装的对象作为类型化对象公开给外部世界。请注意,根据不同的需求,智能指针具有不同的风格。您应该参考Andrei Alexandrescu或Boost库(www. BooStug)SyrdJ.pRT.HPP实现/文档的现代C++编程,以了解更多有关它的内容。希望这能帮助你理解雷伊。