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


回答:


317

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が破壊されたため)、ファイルは自動的に削除されます。


7
多くの文字列実装は、参照カウントされたポインタに関して実装されていることに注意してください。これらのコピーオンライトセマンティクスにより、値による文字列の返却が非常に安価になります。

7
そうでない場合でも、多くのコンパイラはオーバーヘッドを処理するNRV最適化を実装します。一般的に、shared_ptrが役立つことはめったにありません-RAIIに固執し、所有権の共有を避けてください。
Nemanja Trifunovic

27
文字列を返すことは、スマートポインターを実際に使用する良い理由ではありません。戻り値の最適化は戻り値を簡単に最適化でき、c ++ 1x移動セマンティクスはコピーを完全に排除します(正しく使用された場合)。代わりに、実際の例をいくつか示します(たとえば、同じリソースを共有する場合):)
Johannes Schaub-litb 2008

1
なぜJavaがこれを行うことができないのかについての早い段階でのあなたの結論は明確さを欠いていると思います。JavaまたはC#でこの制限を説明する最も簡単な方法は、スタックに割り当てる方法がないためです。C#では、特別なキーワードを使用してスタックを割り当てることができますが、型safteyは失われます。
ApplePieIsGood 2008

4
@Nemanja Trifunovic:このコンテキストでのRAIIとは、スタックにコピーを返す/オブジェクトを作成することを意味しますか?サブクラス化できるタイプのreturn / acceptオブジェクトがある場合、これは機能しません。次に、オブジェクトのスライスを回避するためにポインターを使用する必要があります。これらの場合、スマートポインターの方が生のポインターよりも優れていることが多いと思います。
フランクオスターフェルド2010

141

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

オブジェクトを排他的に所有するスマートポインタです。ブーストにはありませんが、次のC ++標準に登場する可能性があります。それはだコピー不能が、支持体は、名義変更。いくつかのサンプルコード(次のC ++):

コード:

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のようなコピー不可能な(ただし移動可能な)タイプを保持できるためです。

scoped_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

所有権を共有するためのものです。そのため、コピーと移動の両方が可能です。複数のスマートポインターインスタンスが同じリソースを所有できます。リソースを所有する最後のスマートポインターがスコープ外になるとすぐに、リソースは解放されます。私のプロジェクトの1つの実際の例:

コード:

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アクセス​​パスでリソースにアクセスしようとすると、例外がスローされます。


私が知る限り、コピー不可能なオブジェクトは、値のセマンティクスに依存しているため、stlコンテナーでの使用にはまったく適していません。そのコンテナーを並べ替える場合はどうなりますか?sortは要素をコピーします...
fmuecke

C ++ 0xコンテナーはunique_ptr、のような移動のみのタイプを尊重するようにsort変更され、同様に変更されます。
ヨハネスシャウブ-litb

SBRMという言葉を最初に聞いた場所を覚えていますか?ジェームズはそれを追跡しようとしています。
GManNickG 2010

これらを使用するには、どのヘッダーまたはライブラリを含める必要がありますか?これについてさらに読む?
atoMerz

ここで1つのアドバイス:@litbによるC ++の質問への回答がある場合、それは正しい回答です(投票や「正しい」とフラグが付けられている回答に関係なく)...
fnl

32

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

RAIIは、変数がコンストラクターで必要なすべての初期化と、デストラクターで必要なすべてのクリーンアップを確実に処理するための設計パラダイムです。 これにより、すべての初期化とクリーンアップが1つのステップに削減されます。

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

RAIIがC ++で有用である理由は、C ++が、通常のコードフローまたは例外によってトリガーされたスタックの巻き戻しを介して、スコープに出入りする変数の作成と破棄を本質的に管理するためです。それはC ++の景品です。

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

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


さらに、ポインタはRAIIの最も一般的なアプリケーションです。他のリソースよりも数千倍多くのポインタを割り当てる可能性があります。
エクリプス

8

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

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

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

  1. ネットワークソケットは、使用する前に、いつでも(使用したくない場合でも)開きます。RAIIを使用して別の方法で開くことは困難です。RAIIなしでこれを実行しようとすると、MSN接続などの空のソケットを開く可能性があります。次に、「今夜はやりましょう」などのメッセージが転送されず、ユーザーが解雇されず、解雇される危険性があります。
  2. エラーが発生した場合でも、ネットワークソケットを閉じます。ソケットがぶら下がったままになることはありません。これは、「下部に確実にあります」という応答メッセージが送信者に戻るのを妨げる可能性があるためです。

ご覧のように、RAIIは人々がレイドされるのを助けるので、ほとんどの場合非常に便利なツールです。

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


2

Boostには、共有メモリ用のBoost.Interprocessにあるものを含め、これらの数があります。特に5つのプロセスが同じデータ構造を共有している場合など、頭痛の種となる状況では、メモリ管理が大幅に簡略化されます。全員がメモリのチャンクを使い終わったら、自動的に解放され、そこに座って理解する必要はありません。deleteメモリチャンクを呼び出す責任があるはずです。メモリリークが発生したり、ポインタが誤って2回解放されてヒープ全体が破損する可能性があるためです。


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

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

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

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


1
void f(){Obj x; }スタックフレームの作成/破棄(巻き戻し)によってObj xが削除されます...これは、参照カウントとは関係ありません。
エルナン、

参照カウントは、文字列の内部実装の機能です。RAIIは、オブジェクトがスコープから外れた場合のオブジェクト削除の背後にある概念です。問題はRAIIとスマートポインタについてでした。

1
「何が起こっても」-関数が戻る前に例外がスローされるとどうなりますか?
チタニウム

どの関数が返されますか?fooで例外がスローされると、barが削除されます。例外をスローするbarのデフォルトのコンストラクターは、異常なイベントになります。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.