114

C ++開発者は、RAIIとは何か、なぜそれが重要なのか、そして他の言語との関連性があるかどうかについて、よく説明していただけますか?

少し知っています。「ResourceAcquisitionisInitialization」の略だと思います。ただし、その名前は、RAIIが何であるかについての私の(おそらく間違った)理解とは相容れません。RAIIは、スタック上のオブジェクトを初期化する方法であり、それらの変数がスコープ外になると、デストラクタが自動的に実行されるという印象を受けます。リソースをクリーンアップするために呼び出されます。

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

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

ありがとう。

4

10 に答える 10

136

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

RAII は何をすべきかを教えてくれます: コンストラクターでリソースを取得してください! 追加します: 1 つのリソース、1 つのコンストラクター。UTSTTC はそのアプリケーションの 1 つに過ぎず、RAII はそれ以上のものです。

リソース管理は最悪です。ここで、リソースとは、使用後にクリーンアップが必要なものです。多くのプラットフォームにわたるプロジェクトの調査によると、バグの大部分はリソース管理に関連していて、Windows では特に問題が深刻です (オブジェクトとアロケータの種類が多いため)。

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


C++ では、コンストラクターが成功した場合にのみ、デストラクターが呼び出されることが保証されています。それに頼って、RAII は、平均的なプログラマーが気付いていないかもしれない多くの厄介な問題を解決できます。「戻るたびにローカル変数が破棄される」以外の例をいくつか示します。

FileHandleRAII を使用した過度に単純化されたクラスから始めましょう。

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 つのファイルしか開くことができない、両方のファイルを開くことができるがファイルのコピーに失敗する、の 3 つです。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 は扱いにくい場合があります。
Logger が を必要としていると想像してくださいSetTargetFile(const char* target)。その場合、まだ のメンバーである必要があるハンドルはLogger、ヒープ上に存在する必要があります (たとえば、ハンドルの破棄を適切にトリガーするために、スマート ポインター内)。

ガベージコレクションを本当に望んだことはありません。C# をやっていると、気にする必要のない至福の瞬間を感じることがありますが、それ以上に、決定論的破壊によって作成できるすべてのクールなおもちゃが恋しいです。(そのまま使用IDisposableしても切れません。)

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


FileHandle サンプルに関する注意: これは完全なものではなく、単なるサンプルでしたが、正しくないことが判明しました。指摘してくれた Johannes Schaub と、正しい C++0x ソリューションに変えてくれた FredOverflow に感謝します。時間が経つにつれて、ここに記載されているアプローチに落ち着きました。

于 2009-04-03T12:51:29.520 に答える
42

そこには優れた答えがあるので、忘れていたことをいくつか追加します。

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

RAII は次の両方に関するものです。

  1. コンストラクターでリソースを取得し (リソースに関係なく)、デストラクタで取得を解除します。
  2. 変数が宣言されたときにコンストラクターが実行され、変数がスコープ外になったときにデストラクタが自動的に実行されます。

他の方が既に回答されているので、ここでは割愛します。

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

ムッシュー・ジュルダン: なんと!「ニコール、スリッパを持ってきて、ナイトキャップをくれ」と言うのは散文ですか?

哲学の達人: はい、サー。

ムッシュー・ジュルダン: 40年以上、私は散文について何も知らずに話してきました。

— モリエール: 中流階級の紳士、第 2 幕、第 4 場

ムッシュ・ジュールデインが散文で行ったように、C# や Java の人々でさえ、すでに RAII を使用していますが、隠れた方法で使用しています。たとえば、次の Java コード (C# でも同じように に置き換えて記述synchronizedしますlock):

void foo()
{
   // etc.

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

   // etc.
}

... is already using RAII: ミューテックスの取得はキーワード (synchronizedまたはlock) で行われ、取得解除はスコープを出るときに行われます。

RAIIを知らない人でも説明不要なほど自然な表記です。

ここで C++ が Java や C# より優れている点は、RAII を使用して何でも作成できることです。たとえば、C++ には直接的な組み込みのsynchronizednorはありませんlockが、まだそれらを使用できます。

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ですか?

【悲鳴】ファイナルカウントダウンです!

— ヨーロッパ: ファイナル カウントダウン (申し訳ありませんが、引用符がありませんでした。

この句は、C#/Java で使用され、スコープが終了した場合 (例外またはスローfinallyされた例外による) にリソースの破棄を処理します。return

鋭い仕様書の読者は、C++ に finally 節がないことに気付くでしょう。RAII は既にリソースの破棄を処理しているため、C++ では必要ないため、これはエラーではありません。(そして、私を信じてください。C++ デストラクタを記述することは、正しい Java の finally 句や、C# の正しい Dispose メソッドを記述するよりもはるかに簡単です)。

それでも、finally節がクールな場合もあります。C++でできますか?はい、できます!また、RAII を別の方法で使用します。

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

らい?これはC++です!!!

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

C++ である程度の経験を積むと、RAIIの観点から、コンストラクターとデストラクタの自動実行の観点から考えるようになります。

スコープの観点から考え始めると、{および}文字がコード内で最も重要なものになります。

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

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

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

RAII は C++ の大部分を占めており、C++ はそれなしでは C++ とは言えません。

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

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

  • RAII は、GC で処理されるほとんどのケースをすでに処理しています。
  • GC は、純粋なマネージド オブジェクトの循環参照を RAII よりも適切に処理します (ウィーク ポインターのスマートな使用によって軽減されます)。
  • それでも、GC はメモリに制限されていますが、RAII はあらゆる種類のリソースを処理できます。
  • 上で説明したように、RAII はさらに多くのことができます...
于 2011-01-19T21:24:40.777 に答える
10

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

SmartPointer pointer( new ObjectClass() );

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

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

于 2009-04-03T05:28:39.507 に答える
9

以前の回答よりももう少し強く言いたいと思います。

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

于 2009-04-03T08:50:29.100 に答える
8

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

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

RAII の背後にある合理性は、リソース取得の例外を安全にすることです。例外が発生した場所に関係なく、取得したすべてのリソースが適切に解放されること。ただし、これはリソースを取得するクラスの品質に依存します (これは例外セーフでなければならず、これは困難です)。

于 2009-04-03T07:45:31.440 に答える
7

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

于 2009-04-03T05:34:56.543 に答える
4

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

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

于 2009-04-03T06:08:25.883 に答える
1

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

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

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

于 2009-04-03T10:03:25.133 に答える