了解术语和概念的含义

您能否让C ++开发人员详细描述RAII是什么,为什么它很重要,以及它是否与其他语言有任何关联?

我确实知道一点。 我相信它代表“资源获取是初始化”。 然而,这个名字并不与我对RAII的理解(可能是不正确的)有所联系:我认为RAI​​I是一种在堆栈上初始化对象的方式,以便当这些变量超出范围时,析构函数会自动被称为导致资源被清理。

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

你怎么能在堆栈上做些什么来清理堆上的东西呢? 另外,有没有你不能使用RAII的情况? 你有没有发现自己希望收集垃圾? 至少有一个垃圾收集器可以用于某些对象,同时让其他人被管理?

谢谢。


那么,为什么不叫“使用栈来触发清理”(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解决方案。 随着时间的推移,我已经解决了这里记录的方法。


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

0. RAII是关于范围的

RAII是关于两者:

  • 获取构造函数中的资源(不管是什么资源),并在析构函数中取消它。
  • 在声明变量时执行构造函数,并在变量超出作用域时自动执行析构函数。
  • 其他人已经回答了这个问题,所以我不会详细说明。

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

    MONSIEUR JOURDAIN:什么! 当我说,“妮可,给我穿上我的拖鞋,给我睡前用具,”那是散文吗?

    哲学大师:是的,先生。

    MONSIEUR JOURDAIN:四十多年来,我一直在讲散文,却对此毫不知情,我非常感谢你教给我。

    - Molière:中产阶级绅士,第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进行任何操作。 例如,在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缺乏?

    [SHOUTING]这是最后的倒计时!

    - 欧洲:最后的倒计时(对不起,我没有引号,这里... :-)

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

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

    不过,有时候, finally条款会很酷。 我们可以用C ++来做到吗? 我们可以! 再次使用RAII。

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

    RAII? 这是C ++!

    - C ++开发者的愤怒评论,被无名的斯巴达国王和他的300个朋友无耻复制

    当你达到C ++的某种级别的经验时,你开始考虑RAII,就构造者和析构者自动执行而言。

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

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

    数据库部分不可忽略,因为如果您接受支付价格,您甚至可以使用“事务编程”风格编写代码,执行代码行和行,直到最终决定是否要提交所有更改,或者,如果不可能,则将所有更改恢复(只要每条线至少满足强力例外保证)。 (请参阅Herb's Sutter文章的第二部分,了解事务性编程)。

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

    RAII是C ++的重要组成部分,如果没有它,C ++就无法成为C ++。

    这就解释了为什么有经验的C ++开发人员对RAII感到如此迷恋,为什么RAII是他们尝试使用其他语言时首先要搜索的内容。

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

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

  • 请参见:

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

    RAII和C ++中的智能指针

    C ++是否支持“终于”阻止? (我听说过什么'RAII'?)

    RAII与例外

    等等..

    链接地址: http://www.djcxy.com/p/72979.html

    上一篇: Understanding the meaning of the term and the concept

    下一篇: When do we have to use copy constructors?