1. ホーム
  2. c++

[解決済み】C++のRAIIとスマートポインタ

2022-04-12 02:40:23

質問

C++の実務では RAII とは何ですか? スマートポインタ また、RAIIとスマートポインタの組み合わせの利点は何でしょうか。

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

RAIIの簡単な(そしておそらく使い古された)例として、Fileクラスがあります。RAIIがなければ、コードは次のようになります。

File file("/path/to/file");
// Do stuff with file
file.close();

言い換えれば、ファイルを使い終わったら、必ずファイルを閉じなければならない。第一に、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 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()を終了すると削除されてしまいます。つまり、呼び出し側がそのポインタを取得した時点で、そのポインタは役に立たなくなってしまうのです(それを使うと、いろいろとおかしなエラーが発生する可能性があるので、役に立たないよりも悪いと言えるかもしれません)。

では、どうすればいいのか?そうすれば、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);
}

このメソッドが終了すると同時に、file は閉じられ、foo と bar は無効なログファイルを持っていることになります。ヒープ上に file を構築し、foo と bar の両方に file へのポインタを渡せばよいのです。

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 の両方が終了し、file への参照がなくなれば(おそらく foo と bar が破壊されたため)、file は自動的に削除されます。