用語の意味と概念を理解する - RAII(リソース取得は初期化)
-
23-08-2019 - |
質問
C++ 開発者の皆様、RAII とは何なのか、なぜ重要なのか、他の言語との関連性があるのかどうかについて、詳しく説明していただけますか?
私 する 少し知ってください。これは「リソースの取得は初期化である」の略だと思います。ただし、その名前は、RAII が何であるかについての私の (おそらく間違った) 理解と一致しません。RAII はスタック上のオブジェクトを初期化する方法であり、変数がスコープ外になったときにデストラクターが自動的に呼び出され、リソースがクリーンアップされるという印象を受けます。
では、なぜそれが「スタックを使用してクリーンアップをトリガーする」(UTSTTC:) と呼ばれないのでしょうか?そこから「RAⅡ」まではどうやって行くのですか?
そして、ヒープ上に存在するもののクリーンアップを引き起こすものをスタック上に作成するにはどうすればよいでしょうか?また、RAIIが使えない場合はあるのでしょうか?ガベージコレクションをしたいと思うことはありますか?少なくとも、他のオブジェクトを管理しながら一部のオブジェクトに使用できるガベージ コレクターはありますか?
ありがとう。
解決
では、なぜそれが「スタックを使用してクリーンアップをトリガーする」(UTSTTC:) と呼ばれないのでしょうか?
RAII は、何をすべきかを指示しています。コンストラクターでリソースを取得します。私ならこう付け加えます。1 つのリソース、1 つのコンストラクター。UTSTTC はその応用例の 1 つにすぎませんが、RAII はそれ以上のものです。
リソース管理は最悪です。 ここでのリソースとは、使用後にクリーンアップが必要なものすべてです。多くのプラットフォームにわたるプロジェクトの調査によると、バグの大部分はリソース管理に関連しており、Windows では特に深刻です (オブジェクトとアロケータの種類が多いため)。
C++ では、例外と (C++ スタイルの) テンプレートの組み合わせにより、リソース管理が特に複雑になります。ボンネットの下を覗いてみるには、を参照してください。 GOTW8).
C++ はデストラクターが呼び出されることを保証します もし、そしてその場合に限り コンストラクターは成功しました。これに頼って、RAII は平均的なプログラマが気づいていないような多くの厄介な問題を解決できます。ここでは、「ローカル変数は戻るたびに破棄される」以外の例をいくつか示します。
あまりにも単純すぎることから始めましょう FileHandle
RAII を使用するクラス:
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 に感謝します。時間が経つにつれて、私はそのアプローチに落ち着いてきました ここに文書化されています.
他のヒント
素晴らしい答えがたくさんあるので、忘れていたことをいくつか追加するだけです。
0.RAII はスコープに関するものです
RAII には次の両方が含まれます。
- コンストラクターでリソース (どのようなリソースでも) を取得し、デストラクターで取得を解除します。
- 変数が宣言されたときにコンストラクターが実行され、変数がスコープ外になったときにデストラクターが自動的に実行されます。
それについては他の人がすでに答えているので、詳しくは説明しません。
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.
}
...すでに RAII を使用しています:ミューテックスの取得はキーワード(synchronized
または lock
)、スコープから出るときに未取得が行われます。
非常に自然な表記なので、RAII について聞いたことがない人でもほとんど説明の必要はありません。
ここで C++ が Java や C# に比べて優れているのは、RAII を使用して何でも作成できることです。たとえば、以下に直接相当する組み込みの機能はありません。 synchronized
または lock
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++です
ライ?これはC++です!!!
— C++ 開発者の怒りのコメント、無名のスパルタ王とその 300 人の友人が恥知らずにもコピー
C++ の経験がある程度のレベルに達すると、次の観点から考え始めます。 ライ, 、 に関しては コンストラクターとデストラクターの自動実行.
あなたは次の観点から考え始めます スコープ, 、 そしてその {
そして }
文字はコード内で最も重要なものになります。
そして、RAII に関しては、ほとんどすべてが正しく当てはまります。例外の安全性、ミューテックス、データベース接続、データベースリクエスト、サーバー接続、クロック、OS ハンドルなど、そして最後にメモリも重要です。
データベース部分は無視できません。代金を支払うことに同意した場合は、「」と書き込むこともできます。トランザクションプログラミング" スタイルでは、すべての変更をコミットするか、それが不可能な場合はすべての変更を元に戻すかを最終的に決定するまで、コードを何行も実行します (各行が少なくとも強力な例外保証を満たしている限り) )。(この記事の 2 番目の部分を参照してください) ハーブのサッターの記事 トランザクション プログラミングの場合)。
そしてパズルのように、すべてがぴったりと合います。
RAII は C++ の一部であり、RAII なしでは C++ は C++ とは言えません。
これは、経験豊富な C++ 開発者が RAII に夢中になる理由、および別の言語を試すときに最初に RAII を検索する理由を説明しています。
また、ガベージ コレクターがそれ自体は素晴らしいテクノロジーであるにもかかわらず、C++ 開発者の観点からはそれほど印象的ではない理由も説明されています。
- RAII は、GC によって処理されるほとんどのケースをすでに処理しています。
- GC は、純粋な管理対象オブジェクトの循環参照を RAII よりもうまく処理します (弱いポインターを賢く使用することで軽減されます)。
- それでも、GC はメモリに制限されますが、RAII はあらゆる種類のリソースを処理できます。
- 上で説明したように、RAII ではさらに多くのことができます...
RAIIは、リソースを管理するためのC ++デストラクタのセマンティクスを使用しています。例えば、スマートポインタを検討します。あなたは、オブジェクトのアドレスでこのポインタを初期化ポインタのパラメータ化コンストラクタを持っています。
:あなたは、スタック上のポインタを割り当てますSmartPointer pointer( new ObjectClass() );
スマートポインタがスコープの外に出るときは、ポインタクラスのデストラクタは、接続されているオブジェクトを削除します。ポインタは、スタック割り当てとオブジェクト - である。ヒープに割り当てられた
RAIIは助けにはならない特定のケースがあります。あなたは(ブースト:: shared_ptrのような)参照カウントスマートポインタを使用すると、サイクルと、グラフ状の構造体を作成した場合、サイクル内のオブジェクトが解放されることから、互いを防ぐことができますので、たとえば、メモリリークが直面している危険性があります。ガベージコレクションは、この反対役立つだろう。
私はcpitisに同意します。しかし、リソースが何もないだけでメモリできることを追加したいと思います。リソースは、ファイル、クリティカルセクション、スレッドまたはデータベース接続である可能性があります。
リソースの取得は、リソースがリソースを制御するオブジェクトが作成されたとき、コンストラクタが失敗した場合(つまり、例外のため)、取得されたリソースが取得されていないため、初期化ですと呼ばれています。オブジェクトがスコープの外に出た後、そのリソースは解放されます。 C ++の保証ことに成功破壊される構築されているスタック上のすべてのオブジェクト(これは、スーパークラスのコンストラクタが失敗した場合でも、基底クラスのコンストラクタとメンバーが含まれます)。
RAIIの背後にある合理的に資源獲得の例外を安全にすることです。取得したすべてのリソースは例外が発生に関係なく、適切に放出されること。しかし、これは(これは、安全で、これは難しいです例外でなければならない)リソースを取得し、クラスの品質に依存しています。
私は、もう少し強く前の応答をそれを置くしたいと思います。
RAII、のリソース取得された初期化のすべて取得したリソースは、オブジェクトの初期化の文脈で取得しなければならないことを意味します。これは「裸」リソース取得を禁止します。根拠は、オブジェクトベースで動作するC ++のクリーンアップは、関数呼び出しではないという根拠があります。したがって、すべてのクリーンアップは、オブジェクトではなく、関数呼び出しによって行われるべきです。この意味ではC ++は、例えば、次に指向よりオブジェクトでありますJavaの。 Javaのクリーンアップがfinally
句で関数呼び出しに基づいています。
ガベージコレクションの問題点は、RAIIに重要です決定論的破壊を失うということです。変数がスコープの外に出ると、それは、オブジェクトが再利用されるガベージコレクタ次第です。デストラクタが呼び出されるまで、オブジェクトが保持しているのリソースが開催されていきます。
RAIIは、リソース割り当てから来て初期化されています。基本的には、コンストラクタが実行を終了すると、構築されたオブジェクトが完全に初期化し、使用する準備ができていることを意味しています。また、デストラクタは、オブジェクトが所有するすべてのリソース(例えば、メモリ、OSのリソース)を解放することを意味します。
ごみ収集言語/技術(例えばJava(登録商標)、.NET)と比較すると、C ++オブジェクトの生活の完全な制御を可能にします。スタックに割り当てられたオブジェクトのために、あなたは、(実行がスコープ外になったとき)オブジェクトのデストラクタが呼び出されるとき、実際にガベージコレクションの場合には制御されていない事を知っていますよ。でも(例えばブースト:: shared_ptrの)、あなたは尖ったオブジェクトへの参照が存在しない場合には、そのオブジェクトのデストラクタが呼び出されることを知っているよC ++でスマートポインタを使用します。
そして、どのように、あなたはヒープ上に住んでいる何かのクリーンアップの原因となりますスタック上に何かを作ることができますか?
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を使用しない場合がありますか?
いや、そうでもない。
あなたは今まで自分がガベージコレクションを願うと思いますか?少なくとも、他人をさせながらあなたには、いくつかのオブジェクトのために使用することができますガベージコレクタを管理する?
ネヴァー。ガベージコレクションは、動的リソース管理の非常に小さなサブセットを解決します。
があり良い答えの多くはここではすでにありますが、私は追加したい:
RAIIの簡単な説明は、それが範囲の外に出るたびにC ++で、スタック上に割り当てられたオブジェクトが破棄される、ということです。ことを意味し、オブジェクトのデストラクタが呼び出され、すべての必要なクリーンアップを行うことができます。
オブジェクトは、「新」、いいえ要求され、「削除」しないで作成された場合には、意味します。そして、これはまた、「スマートポインタ」の背後にある考え方である - 。彼らは、スタック上に存在し、かつ本質的にヒープベースのオブジェクトをラップ
RAIIは、リソースの取得された初期の頭文字をとったものです。
この手法は非常にユニークな引数が渡されていることを一致しているコンストラクタまたは明示的に提供がそう呼ばれた場合、デフォルトコンストラクタがデストラクタ&呼ばれ、最悪の場合、ほぼ自動的のでコンストラクタとデストラクタ&両方のための彼らのサポートのC ++にありますあなたがC ++クラスに明示的にデストラクタを書かなかった場合はC ++コンパイラによって追加されたデフォルトの1が呼び出されます。無料のストアを使用していない意味(メモリが割り当てられた/新しいを使用して割り当てを解除、[] /削除新しい、[] C ++の演算子を削除) - これが唯一の自動管理されているC ++オブジェクトに対して発生します。
RAII技術がexplcitly使用してより多くのメモリを求めることで、ヒープ/フリーストアで作成されたオブジェクトを処理するために、この自動管理オブジェクト機能を利用した新/新しい[]、明示的に削除を呼び出すことによって破壊されるべき/ []削除してください。自動管理オブジェクトのクラスは、ヒープ/フリーストアメモリ上に作成され、この別のオブジェクトをラップします。包まれている自動管理オブジェクトのコンストラクタが実行されたときにそのため、ラップされたオブジェクトをヒープ/フリーストアメモリ上に作成さ&自動管理オブジェクトのハンドルがスコープの外に出たとき、その自動管理オブジェクトのデストラクタが自動的に呼び出されますオブジェクトが削除使用して破壊されます。あなたはプライベートスコープ内の別のクラス内のそのようなオブジェクトをラップする場合はOOPの概念を、あなたはラップクラスのメンバー&メソッドへのアクセスを持っていないだろうと、これはスマートポインタが(別名クラスを扱う)のために設計されている理由です。これらのスマートポインタが露出し、メモリオブジェクトがで構成されて任意のメンバー/メソッドを呼び出すできるようにすることで、そこに外部の世界とに型指定されたオブジェクトとしてラップされたオブジェクトを公開します。スマートポインタは、異なるニーズに基づいて、さまざまな味を持っていることに注意してください。あなたはアンドレイアレキによって現代C ++プログラミングを参照するか、それについての詳細を学ぶために、ライブラリの(www.boostorg)shared_ptr.hpp実装/ドキュメントを高める必要があります。これはあなたがRAIIを理解するのに役立ちます願っています。