1. ホーム
  2. c++

[解決済み] 用語の意味と概念の理解 - RAII (Resource Acquisition is Initialization)

2022-10-07 07:47:59

質問

C++の開発者の方々は、RAIIとは何か、なぜそれが重要なのか、そして他の言語との関連はあるのかどうか、きちんと説明していただけませんか?

I する は少し知っています。私は、それが "Resource Acquisition is Initialization" の略であると信じています。しかし、この名前は RAII が何であるかについての私の理解とは一致しません(おそらく間違っています)。RAII は、スタック上のオブジェクトを初期化する方法であり、それらの変数がスコープ外に出たときに、デストラクタが自動的に呼び出されてリソースがクリーンアップされるという印象を受けます。

では、なぜそれが "using the stack to trigger cleanup" (UTSTTC:) と呼ばれないのでしょうか? そこからどのようにして "RAII" に至るのでしょうか?

そして、ヒープに住んでいる何かのクリーンアップを引き起こすスタック上の何かをどのように作ることができますか?また、RAIIを使えないケースもあるのでしょうか?ガベージコレクションがあればと思うことはありませんか?少なくとも、あるオブジェクトにはガベージコレクタを使用し、他のオブジェクトは管理できるようにしたいと思ったことはありませんか?

ありがとうございます。

どのように解決するのですか?

<ブロッククオート

では、なぜ "using the stack to trigger cleanup" (UTSTTC:) と呼ばれないのでしょうか?

RAIIは何をすべきかを教えてくれているのです。コンストラクタでリソースを取得することです。1つのリソース、1つのコンストラクタ。UTSTTCはその1つの応用で、RAIIはそれ以上です。

リソース管理は最悪です。 ここで、リソースとは、使用後にクリーンアップが必要なものすべてを指します。多くのプラットフォームにわたるプロジェクトの研究では、バグの大部分はリソース管理に関連しており、Windows では特にひどい (多くの種類のオブジェクトとアロケータがあるため) ことが分かっています。

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


C++では、デストラクタを呼び出す際に である場合にのみ であることを保証しています。これに依存して、RAIIは平均的なプログラマが気づかないような多くの厄介な問題を解決することができます。以下は、quot; my local variables will be destroyed whenever I return"を超えるいくつかの例です。

まず、あまりにも単純化された 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"));
}

処理すべきエラーは、ファイルが開けない、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は、遅延取得が必要な場合、集約されたオブジェクトをヒープに押し付けるため、扱いにくいことがあります。

ロガーが SetTargetFile(const char* target) . その場合、ハンドルは、やはり Logger のメンバーである必要があり、ヒープ上に存在する必要があります (例えば、スマートポインターで、ハンドルの破壊を適切にトリガーするためです)。

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

私は、GC の恩恵を受けるかもしれない、特に複雑な構造を 1 つ持っていました。そこでは、単純なスマート ポインターが複数のクラスにわたって循環参照を引き起こします。私たちは、強いポインターと弱いポインターのバランスを注意深くとることで乗り切りましたが、何かを変更したいときはいつでも、大きな関係図を研究しなければなりません。GC はより良かったかもしれませんが、いくつかのコンポーネントは、早急にリリースされるべきリソースを保持していました。


FileHandle サンプルについてのメモです。これは完全なものではなく、単なるサンプルでしたが、不正確であることが判明しました。指摘してくれた Johannes Schaub と、それを正しい C++0x ソリューションに変えてくれた FredOverflow に感謝します。時間をかけて、私は次のようなアプローチに落ち着きました。 ここに記載されている .