web-dev-qa-db-ja.com

C ++のRAIIとスマートポインター

実際にC++を使用する場合、 [〜#〜] raii [〜#〜] とは何ですか、 スマートポインター とは何ですか、これらはプログラムにどのように実装され、どのようなメリットがありますかスマートポインターでRAIIを使用する方法

189
Rob Kam

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つの理由から完全ではありません。まず、文字列の実装により、文字列のコピーは安価になる傾向があります。第二に、名前付き戻り値の最適化として知られているもののため、値で返すことは、コンパイラが物事をスピードアップするためにいくらかの賢明さを行うことができるため、高価ではないかもしれません。

それでは、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に無効なログファイルが含まれるようになります。ヒープ上にファイルを作成し、fileへのポインターを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が破壊されるため)、ファイルは自動的に削除されます。

311

前提と理由は、概念的には単純です。

RAIIは、変数がコンストラクターで必要なすべての初期化とデストラクタで必要なすべてのクリーンアップを処理することを保証する設計パラダイムです。これにより、すべての初期化およびクリーンアップが単一段階。

C++はRAIIを必要としませんが、RAIIメソッドを使用するとより堅牢なコードが生成されることがますます受け入れられています。

RAIIがC++で役立つ理由は、通常のコードフローまたは例外によってトリガーされるスタックのアンワインドを介して、C++がスコープの出入り時に変数の作成と破棄を本質的に管理するためです。これはC++の景品です。

すべての初期化とクリーンアップをこれらのメカニズムに結び付けることにより、C++がこの作業も確実に処理するようになります。

C++でRAIIについて話すと、通常、スマートポインターの議論につながります。これは、クリーンアップに関してはポインターが特に壊れやすいためです。 mallocまたはnewから取得したヒープに割り当てられたメモリを管理する場合、通常、ポインタが破棄される前にそのメモリを解放または削除するのはプログラマの責任です。スマートポインターはRAII哲学を使用して、ポインター変数が破棄されるたびにヒープに割り当てられたオブジェクトが破棄されるようにします。

32
Drew Dormann

スマートポインターはRAIIのバリエーションです。 RAIIは、リソースの取得が初期化であることを意味します。スマートポインタは、使用前にリソース(メモリ)を取得し、デストラクタで自動的に破棄します。次の2つのことが起こります。

  1. memoryを使用する前に、常に気に入らない場合でも割り当てます-スマートポインターを使用して別の方法を実行するのは困難です。これが発生していなかった場合は、NULLメモリにアクセスしようとするため、クラッシュが発生します(非常に苦痛です)。
  2. エラーがあってもmemoryを解放します。メモリがぶら下がっていません。

たとえば、別の例はネットワークソケットRAIIです。この場合:

  1. ネットワークソケットを使用する前に、常に気に入らない場合でも-RAIIで別の方法で行うのは困難です。 RAIIを使用せずにこれを実行しようとすると、MSN接続などのために空のソケットを開く可能性があります。その場合、「今夜やってみましょう」などのメッセージは転送されない可能性があり、ユーザーはレイドされず、解雇される危険性があります。
  2. エラーがある場合でも、ネットワークソケットを閉じます。ソケットがハングしたままになることはありません。これにより、応答メッセージ「sill ill be bottom on be bottom」が送信者に戻されなくなる可能性があります。

さて、ご覧のとおり、RAIIは人々がレイトするのに役立つため、ほとんどの場合非常に便利なツールです。

スマートポインターのC++ソースは、私の周りの応答を含め、ネット上で数百万単位です。

8
mannicken

Boostには、共有メモリの Boost.Interprocess にあるものを含む、これらの多くがあります。特に、5つのプロセスが同じデータ構造を共有している場合など、頭痛を誘発する状況では、メモリ管理が大幅に簡素化されます。誰がメモリチャンクでdeleteを呼び出すか、メモリリーク、または誤って2回解放されてヒープ全体を破損する可能性のあるポインタにならないようにする必要があります。

2
Jason S
 void foo()
 {
 std :: string bar; 
 //
 //ここにコードを追加
 // 
} 

何が起ころうとも、foo()関数のスコープが残されると、barは適切に削除されます。

内部的には、std :: string実装では、参照カウントポインターがよく使用されます。したがって、文字列のコピーの1つが変更された場合にのみ、内部文字列をコピーする必要があります。したがって、参照カウントのスマートポインターを使用すると、必要なときにのみ何かをコピーできます。

さらに、内部参照カウントにより、内部文字列のコピーが不要になったときにメモリが適切に削除される可能性があります。

0
Juan