web-dev-qa-db-ja.com

用語の意味と概念を理解する-RAII(Resource Acquisition is Initialization)

C++開発者に、RAIIとは何か、なぜそれが重要なのか、そして他の言語との関連性があるかどうかについて、適切な説明をお願いしますか?

私はdo少し知っています。 「リソース獲得は初期化」の略だと思います。ただし、その名前はRAIIが何であるか(おそらく正しくない)を理解しているとは言えません。RAIIはスタック上のオブジェクトを初期化する方法であり、これらの変数がスコープから外れると、デストラクターが自動的にリソースがクリーンアップされる原因となる。

それでは、なぜ「スタックを使用してクリーンアップをトリガーする」(UTSTTC :)と呼ばれないのですか?そこから「らい」までどうやって行くの?

そして、ヒープ上に存在する何かをクリーンアップするスタック上で何かをどのように作成できますか?また、RAIIが使えない場合はありますか?ガベージコレクションを希望していることはありますか?少なくとも、他のオブジェクトを管理しながら、いくつかのオブジェクトに使用できるガベージコレクターはありますか?

ありがとう。

109
Charlie Flowers

それでは、なぜ「スタックを使用してクリーンアップをトリガーする」(UTSTTC :)と呼ばれないのですか?

RAIIが何をすべきかを指示しています:コンストラクターでリソースを取得してください!追加します:1つのリソース、1つのコンストラクター。 UTSTTCはそのアプリケーションの1つにすぎません。RAIIはそれだけではありません。

Resource Management sucks。ここで、リソースは使用後にクリーンアップが必要なものです。多くのプラットフォームにまたがるプロジェクトの調査によると、バグの大部分はリソース管理に関連しており、Windowsでは特に多くの種類のオブジェクトとアロケータが原因です。

C++では、例外と(C++スタイル)テンプレートの組み合わせにより、リソース管理が特に複雑になります。中身をのぞくには、 GOTW8 を参照してください。


C++は、デストラクタが呼び出されることを保証しますif if only if if]コンストラクタが成功した。これに依存して、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"));
}
_

処理するエラーケースは3つあります。ファイルを開くことができない、1つのファイルしか開くことができない、両方のファイルを開くことができるが、ファイルのコピーに失敗した。 RAII以外の実装では、Fooは3つのケースすべてを明示的に処理する必要があります。

RAIIは、1つのステートメント内で複数のリソースが取得された場合でも、取得されたリソースを解放します。

次に、いくつかのオブジェクトを集約します。

_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の恩恵を受けた可能性のある特に複雑な構造を1つ持っていました。この場合、「単純な」スマートポインタが複数のクラスに対する循環参照を引き起こします。強力なポインターと弱いポインターのバランスを慎重にとることで混乱しましたが、何かを変更したいときはいつでも、大きな関係図を調査する必要があります。 GCの方が良かったかもしれませんが、一部のコンポーネントはできるだけ早くリリースする必要があるリソースを保持していました。


FileHandleサンプルに関するメモ:完全なものではなく、単なるサンプルでしたが、正しくないことが判明しました。指摘してくれたJohannes Schaubと、それを正しいC++ 0xソリューションに変えてくれたFredOverflowに感謝します。時間の経過とともに、私はアプローチで解決しました ここに文書化されています

131
peterchen

すばらしい答えがあるので、忘れてしまったことをいくつか追加します。

0. RAIIはスコープに関するものです

RAIIは両方についてです:

  1. コンストラクターで(どのリソースでも)リソースを取得し、デストラクターでリソースを解放する。
  2. 変数が宣言されたときにコンストラクターが実行され、変数がスコープから外れたときにデストラクターが自動的に実行されます。

他の人はすでにそれについて答えたので、詳しくは述べません。

1. JavaまたはC#でコーディングする場合、すでにRAIIを使用しています...

MONSIEUR JOURDAIN:なんと! 「ニコール、スリッパを持ってきて、ナイトキャップをくれ」って言ったら、それは散文ですか?

哲学のマスター:はい、卿。

MONSIEUR JOURDAIN:40年以上の間、私はそれについて何も知らずに散文を話してきました、そして私はあなたにそれを教えてくれたことに対して多くの義務があります。

—モリエール:中産階級の紳士、第2幕、シーン4

Monsieur 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を使用しています:mutexの取得はキーワード(synchronizedまたはlock)で行われ、スコープの終了時に未取得が行われます。

その表記法はとても自然で、RAIIについて聞いたことがない人でもほとんど説明を必要としません。

C++の利点はJavaであり、ここでのC#は、RAIIを使用して何でも作成できることです。たとえば、synchronizedlockは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年)

(オブジェクト宣言で)コンストラクターが呼び出されるタイミングと、対応するデストラクターが(スコープの出口で)呼び出されるタイミングがわかっているため、1行だけでほぼ魔法のコードを記述できます。 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句がないことに気づくでしょう。また、RAIIはすでにリソースの破棄を処理しているため、C++では必要ないため、これはエラーではありません。 (そして私を信じて、C++デストラクタを書くことは、正しいJava finally節、あるいはC#の正しいDisposeメソッドでさえ)書くよりもはるかに簡単です。

それでも、finally句が便利な場合があります。 C++でできますか? はい、できます! そして、RAIIを別の方法で使用します。

結論:RAIIはC++の哲学以上のものです:それはC++です

RAII?これIS C++ !!!

— C++開発者の憤慨したコメント、あいまいなスパルタ王と彼の300人の友人によって恥知らずにコピーされた

C++である程度の経験を積んだら、 RAII、 の面では コンストラクタとデストラクタの自動実行

あなたはの面で考え始める スコープ、 そしてその {および}文字は、コードで最も重要な文字になります。

そして、RAIIの点でほぼすべてが適切に適合します。例外の安全性、ミューテックス、データベース接続、データベース要求、サーバー接続、クロック、OSハンドルなど、そして最後に、メモリです。

データベース部分は無視できません。価格の支払いを受け入れると、「トランザクションプログラミング"スタイル、最終的にすべての変更をコミットするか、可能でない場合はすべての変更を元に戻すかを決定するまで、行とコード行を実行します(各行が少なくとも強力な例外保証を満たす限り、 )(トランザクションプログラミングについては、この2番目の部分 HerbのSutterの記事 を参照してください)。

そして、パズルのように、すべてが収まります。

RAIIはC++の大部分を占めているため、C++はそれなしではC++にはなり得ません。

これは、経験豊富なC++開発者がRAIIに非常に夢中になる理由と、RAIIが別の言語を試すときに最初に検索する理由であることを説明しています。

そしてそれは、ガベージコレクター自体が素晴らしい技術であるにもかかわらず、C++開発者の観点からはそれほど印象的でない理由を説明しています。

  • RAIIは、GCで処理されたほとんどのケースをすでに処理しています
  • GCは、純粋な管理対象オブジェクトの循環参照を使用するRAIIよりも扱いが良い(弱いポインターのスマートな使用によって軽減される)
  • それでも、GCはメモリに制限されていますが、RAIIはあらゆる種類のリソースを処理できます。
  • 上記のように、RAIIはさらに多くのことができます...
42
paercebal
16
Mitch Wheat

RAIIはC++デストラクタセマンティクスを使用してリソースを管理しています。たとえば、スマートポインターについて考えてみます。オブジェクトのアドレスでこのポインターを初期化するポインターのパラメーター化されたコンストラクターがあります。スタックにポインタを割り当てます。

SmartPointer pointer( new ObjectClass() );

スマートポインターがスコープから外れると、ポインタークラスのデストラクターが接続されたオブジェクトを削除します。ポインタはスタックに割り当てられ、オブジェクトはヒープに割り当てられます。

RAIIが役に立たない場合があります。たとえば、参照カウントスマートポインター(boost :: shared_ptrなど)を使用して、サイクルのあるグラフのような構造を作成すると、サイクル内のオブジェクトがお互いに解放されなくなるため、メモリリークに直面するリスクがあります。ガベージコレクションはこれを防ぐのに役立ちます。

10
sharptooth

前回の回答よりももう少し強くお願いします。

RAII、Resource Acquisition Is Initializationは、取得したすべてのリソースをオブジェクトの初期化のコンテキストで取得する必要があることを意味します。これは、「裸の」リソース獲得を禁止します。その理由は、C++でのクリーンアップは、関数呼び出しではなくオブジェクトベースで機能するということです。したがって、すべてのクリーンアップは、関数呼び出しではなく、オブジェクトによって実行される必要があります。この意味で、C++はよりオブジェクト指向です。 Java。 Javaクリーンアップは、finally句の関数呼び出しに基づいています。

9
MSalters

私はcpitisに同意します。ただし、リソースはメモリだけでなく、何でもかまいません。リソースは、ファイル、クリティカルセクション、スレッド、またはデータベース接続です。

これは、リソースを制御するオブジェクトの構築時にリソースが取得されるため、リソース取得は初期化と呼ばれます。コンストラクターが失敗した場合(つまり、例外のため)、リソースは取得されません。次に、オブジェクトがスコープ外になると、リソースが解放されます。 c ++は、正常に構築されたスタック上のすべてのオブジェクトが破棄されることを保証します(これには、スーパークラスコンストラクターが失敗した場合でも、基本クラスとメンバーのコンストラクターが含まれます)。

RAIIの背後にある合理的な理由は、リソース取得の例外を安全にすることです。取得したすべてのリソースが、どこで例外が発生しても適切に解放されること。ただし、これはリソースを取得するクラスの品質に依存します(これは例外セーフである必要があり、これは困難です)。

8
iain

ガベージコレクションの問題は、RAIIにとって重要な決定論的破壊を失うことです。変数がスコープ外になると、オブジェクトが再利用されるのはガベージコレクタに任されます。オブジェクトが保持しているリソースは、デストラクタが呼び出されるまで保持され続けます。

7
Mark Ransom

RAIIはResource Allocation Is Initializationから来ています。基本的に、これは、コンストラクターが実行を完了すると、構築されたオブジェクトが完全に初期化され、使用できるようになることを意味します。また、デストラクタがオブジェクトが所有するすべてのリソース(メモリ、OSリソースなど)を解放することも意味します。

ガベージコレクションされた言語/テクノロジ(Java、.NETなど)と比較すると、C++ではオブジェクトの寿命を完全に制御できます。スタックに割り当てられたオブジェクトの場合、オブジェクトのデストラクタがいつ呼び出されるか(実行がスコープ外になるとき)、ガベージコレクションの場合は実際には制御されないことがわかります。 C++でスマートポインター(例:boost :: shared_ptr)を使用しても、ポイントされたオブジェクトへの参照がない場合は、そのオブジェクトのデストラクターが呼び出されることがわかります。

4

そして、ヒープ上に存在する何かをクリーンアップするスタック上で何かをどのように作成できますか?

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が使えない場合はありますか?

いいえ、そうでもありません。

ガベージコレクションを希望していることはありますか?少なくとも、他のオブジェクトを管理しながら、いくつかのオブジェクトに使用できるガベージコレクターはありますか?

決して。ガベージコレクションは、動的リソース管理の非常に小さなサブセットのみを解決します。

3
Rob K

ここにはすでに良い答えがたくさんありますが、追加したいと思います。
RAIIの簡単な説明は、C++では、スタックに割り当てられたオブジェクトがスコープ外になると破棄されるということです。つまり、オブジェクトデストラクタが呼び出され、必要なクリーンアップをすべて実行できます。
つまり、「新規」なしでオブジェクトが作成された場合、「削除」は必要ありません。そして、これは「スマートポインタ」の背後にある考え方でもあります。それらはスタック上に存在し、基本的にヒープベースのオブジェクトをラップします。

2
E Dominique

RAIIはResource Acquisition Is Initializationの頭字語です。

この手法はC++に非常に独特です。これは、コンストラクタとデストラクタの両方と、渡される引数に一致するコンストラクタ、またはデフォルトコンストラクタが呼び出される最悪の場合にデフォルトコンストラクタが呼び出される&デストラクタがほとんど自動的にサポートされるためです。 C++コンパイラによって追加されたものは、C++クラスのデストラクタを明示的に記述しなかった場合に呼び出されます。これは、自動管理されているC++オブジェクト、つまり、フリーストア(new、new []/delete、delete [] C++演算子を使用して割り当て/割り当て解除されたメモリ)を使用していない場合にのみ発生します。

RAIIテクニックは、この自動管理オブジェクト機能を利用して、new/new []を使用してヒープ/フリーストアに作成されたオブジェクトを明示的に要求し、delete/delete []を呼び出して明示的に破棄する必要があります。 。自動管理オブジェクトのクラスは、ヒープ/空きストアメモリに作成されたこの別のオブジェクトをラップします。したがって、自動管理オブジェクトのコンストラクターが実行されると、ラップされたオブジェクトがヒープ/フリーストアメモリ上に作成され、自動管理オブジェクトのハンドルがスコープから外れると、その自動管理オブジェクトのデストラクタが自動的に呼び出されます。オブジェクトは削除を使用して破棄されます。 OOPの概念では、そのようなオブジェクトをプライベートスコープ内の別のクラス内にラップすると、ラップされたクラスのメンバーとメソッドにアクセスできなくなります。これがスマートポインター(別名ハンドルクラス)の理由です。これらのスマートポインターは、ラップされたオブジェクトを型付きオブジェクトとして外部の世界に公開し、そこから、公開されたメモリオブジェクトを構成するメンバー/メソッドを呼び出すことができます。スマートポインターには、さまざまなニーズに基づいてさまざまなフレーバーがあることに注意してください。 Andrei AlexandrescuによるModern C++プログラミングまたはブーストライブラリ(www.boostorg)のshared_ptr.hpp実装/ドキュメントを参照して、RAIIの理解に役立ててください。

1
techcraver