回答:
RAIIの簡単な(そしておそらく使いすぎた)例はFileクラスです。RAIIがない場合、コードは次のようになります。
File file("/path/to/file");
// Do stuff with file
file.close();
つまり、ファイルを使い終わったら、必ずファイルを閉じる必要があります。これには2つの欠点があります。まず、Fileを使用する場合は常にFile :: close()を呼び出さなければなりません。これを忘れると、必要以上に長くファイルを保持してしまうことになります。2番目の問題は、ファイルを閉じる前に例外がスローされた場合はどうなりますか?
Javaは、finally節を使用して2番目の問題を解決します。
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
またはJava 7以降、try-with-resourceステートメント:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++は、RAIIを使用して両方の問題を解決します。つまり、Fileのデストラクタでファイルを閉じます。Fileオブジェクトが適切なタイミングで破棄される限り(それはとにかくそれであるはずです)、ファイルを閉じる処理が行われます。したがって、コードは次のようになります。
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
オブジェクトがいつ破棄されるかは保証されないため、Javaでは実行できません。ファイルなどのリソースがいつ解放されるかは保証できません。
スマートポインター-多くの場合、スタック上にオブジェクトを作成します。例えば(そして別の答えから例を盗む):
void foo() {
std::string str;
// Do cool things to or using str
}
これは正常に機能しますが、strを返す場合はどうでしょうか。これを書くことができます:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
それで、何が問題になっていますか?戻り値の型はstd :: stringです。つまり、値で返すということです。つまり、strをコピーし、実際にコピーを返します。これは高価になる可能性があるため、コピーのコストを避けたい場合があります。したがって、参照またはポインターで戻るという考えが浮かぶかもしれません。
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
残念ながら、このコードは機能しません。strへのポインタを返していますが、strはスタック上に作成されているため、foo()を終了すると削除されます。言い換えると、呼び出し元がポインタを取得するまでに、それは役に立たない(そして、それを使用するとあらゆる種類のファンキーなエラーが発生する可能性があるため、おそらく役に立たないよりも悪い)
それで、解決策は何ですか?newを使用してヒープ上にstrを作成することができます-そうすれば、foo()が完了しても、strは破棄されません。
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
もちろん、このソリューションも完璧ではありません。その理由は、strを作成したが、それを削除したことはないからです。これは非常に小さなプログラムでは問題にならないかもしれませんが、一般的には、確実に削除する必要があります。呼び出し元がオブジェクトを使い終わったら、そのオブジェクトを削除する必要があると言えます。欠点は、呼び出し側がメモリを管理する必要があることです。これにより、複雑さが増し、エラーが発生してメモリリークが発生する可能性があります。つまり、オブジェクトが不要になってもオブジェクトが削除されません。
これがスマートポインタの出番です。次の例ではshared_ptrを使用しています。実際に何を使用したいかを知るために、さまざまなタイプのスマートポインタを確認することをお勧めします。
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
現在、shared_ptrはstrへの参照の数をカウントします。例えば
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
同じ文字列への参照が2つあります。strへの参照が残っていない場合は削除されます。そのため、自分で削除することを心配する必要はありません。
簡単な編集:一部のコメントで指摘されているように、この例は(少なくとも!)2つの理由で完璧ではありません。まず、文字列の実装により、文字列のコピーは安価になる傾向があります。第2に、名前付き戻り値の最適化と呼ばれるもののため、コンパイラーは処理を高速化するためにいくらか賢いので、値による戻りは高価ではない場合があります。
それでは、Fileクラスを使用して別の例を試してみましょう。
ファイルをログとして使用するとします。つまり、ファイルを追加専用モードで開きます。
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
次に、ファイルを他のいくつかのオブジェクトのログとして設定します。
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
残念ながら、この例はひどく終了します-このメソッドが終了するとすぐにファイルが閉じます。つまり、fooとbarに無効なログファイルが存在することになります。ヒープ上にファイルを作成し、ファイルへのポインタをfooとbarの両方に渡すことができます。
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
しかし、誰がファイルを削除する責任がありますか?どちらもファイルを削除しない場合、メモリとリソースの両方にリークがあります。fooとbarのどちらが先にファイルで終了するかはわからないため、ファイル自体を削除することもできません。たとえば、barがファイルを完了する前にfooがファイルを削除した場合、barに無効なポインターが含まれるようになります。
したがって、ご想像のとおり、スマートポインタを使用して支援することができます。
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
これで、ファイルの削除について心配する必要はありません。fooとbarの両方が終了し、ファイルへの参照がなくなると(おそらくfooとbarが破壊されたため)、ファイルは自動的に削除されます。
RAIIこれはシンプルだが素晴らしいコンセプトの奇妙な名前です。Scope Bound Resource Management(SBRM)という名前がより良いです。多くの場合、ブロックの先頭でリソースを割り当て、ブロックの出口でリソースを解放する必要があるという考えです。ブロックの終了は、通常のフロー制御、ブロックからのジャンプ、および例外によっても発生する可能性があります。これらすべてのケースをカバーするために、コードはより複雑で冗長になります。
SBRMなしでそれを行う例にすぎません:
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
ご覧のように、私たちが突っ込む方法はたくさんあります。アイデアは、リソース管理をクラスにカプセル化することです。オブジェクトの初期化によりリソースが取得されます(「リソースの取得は初期化」)。ブロック(ブロックスコープ)を終了すると、リソースは再び解放されます。
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
リソースの割り当て/割り当て解除だけを目的としない独自のクラスを取得している場合、これは便利です。割り当ては、彼らの仕事を成し遂げるための追加の関心事に過ぎません。しかし、リソースを割り当てたり割り当て解除したりするだけですぐに、上記は不便になります。取得するあらゆる種類のリソースに対して、ラッピングクラスを記述する必要があります。それを容易にするために、スマートポインターを使用すると、そのプロセスを自動化できます。
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
通常、スマートポインターはnew / deleteの薄いラッパーでdelete
、所有するリソースがスコープ外に出たときに呼び出されます。shared_ptrのような一部のスマートポインターを使用すると、の代わりに使用される、いわゆるdeleterを通知できますdelete
。たとえば、shared_ptrに適切な削除機能について通知する限り、ウィンドウハンドル、正規表現リソース、その他の任意のものを管理できます。
さまざまな目的のためのさまざまなスマートポインターがあります。
コード:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
auto_ptrとは異なり、unique_ptrはコンテナーに入れることができます。これは、コンテナーがストリームやunique_ptrのようなコピー不可能な(ただし移動可能な)タイプを保持できるためです。
コード:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
コード:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
ご覧のとおり、plot-source(関数fx)は共有されていますが、それぞれに個別のエントリがあり、そこに色を設定しています。コードがスマートポインタが所有するリソースを参照する必要があるが、リソースを所有する必要がない場合に使用されるweak_ptrクラスがあります。生のポインタを渡す代わりに、weak_ptrを作成する必要があります。リソースを所有しているshared_ptrがなくなったとしても、weak_ptrアクセスパスでリソースにアクセスしようとすると、例外がスローされます。
unique_ptr
、のような移動のみのタイプを尊重するようにsort
変更され、同様に変更されます。
RAIIは、変数がコンストラクターで必要なすべての初期化と、デストラクターで必要なすべてのクリーンアップを確実に処理するための設計パラダイムです。 これにより、すべての初期化とクリーンアップが1つのステップに削減されます。
C ++ではRAIIは必要ありませんが、RAIIメソッドを使用するとより堅牢なコードが生成されることがますます受け入れられています。
RAIIがC ++で有用である理由は、C ++が、通常のコードフローまたは例外によってトリガーされたスタックの巻き戻しを介して、スコープに出入りする変数の作成と破棄を本質的に管理するためです。それはC ++の景品です。
すべての初期化とクリーンアップをこれらのメカニズムに結び付けることにより、C ++がこの作業も同様に処理することが保証されます。
C ++でRAIIについて話すと、通常、ポインタはクリーンアップに関して特に壊れやすいため、スマートポインタの説明につながります。mallocまたはnewから取得したヒープ割り当てメモリを管理する場合、通常、ポインタが破棄される前にそのメモリを解放または削除するのはプログラマの責任です。スマートポインターは、RAIIの哲学を使用して、ポインター変数が破棄されるたびにヒープに割り当てられたオブジェクトが確実に破棄されるようにします。
スマートポインターはRAIIのバリエーションです。RAIIは、リソースの取得が初期化であることを意味します。スマートポインタは、使用前にリソース(メモリ)を取得し、デストラクタで自動的に破棄します。2つのことが起こります。
たとえば、別の例はネットワークソケットRAIIです。この場合:
ご覧のように、RAIIは人々がレイドされるのを助けるので、ほとんどの場合非常に便利なツールです。
スマートポインターのC ++ソースは、私の上の応答を含め、ネット全体で数百万です。
Boostには、共有メモリ用のBoost.Interprocessにあるものを含め、これらの数があります。特に5つのプロセスが同じデータ構造を共有している場合など、頭痛の種となる状況では、メモリ管理が大幅に簡略化されます。全員がメモリのチャンクを使い終わったら、自動的に解放され、そこに座って理解する必要はありません。delete
メモリチャンクを呼び出す責任があるはずです。メモリリークが発生したり、ポインタが誤って2回解放されてヒープ全体が破損する可能性があるためです。
void foo() { std :: string bar; // //ここにコードを追加 // }
何が起こっても、foo()関数のスコープが残されると、barは適切に削除されます。
内部的にstd :: string実装は、参照カウントポインターを使用することがよくあります。したがって、内部文字列は、文字列のコピーの1つが変更されたときにのみコピーする必要があります。したがって、参照カウントされたスマートポインターは、必要な場合にのみ何かをコピーすることを可能にします。
さらに、内部参照カウントにより、内部文字列のコピーが不要になったときにメモリが適切に削除される可能性があります。