第11章: モダンC++の流儀:RAIIとスマートポインタ

これまでの章で、newdelete を使った動的なメモリ管理を学びました。しかし、これらの手動管理は delete の呼び忘れによるメモリリークや、複雑なコードでのリソース管理の煩雑さを引き起こす原因となりがちです。

C++11以降の「モダンC++」では、こうした問題を解決するための洗練された仕組みが導入されました。この章では、エラーハンドリングのための例外処理、リソース管理の基本思想である RAIIイディオム、そしてそれを具現化するスマートポインタ (std::unique_ptr, std::shared_ptr) について学び、より安全で堅牢なコードを書くための流儀を身につけます。

例外処理: try, catch を使ったエラーハンドリング

プログラムでは、ファイルの読み込み失敗やメモリ確保の失敗など、予期せぬエラーが発生することがあります。C++では、このような状況を処理するために例外 (Exception) という仕組みが用意されています。

例外処理は、以下の3つのキーワードで構成されます。

  • throw: 例外的な状況が発生したことを知らせるために、例外オブジェクトを「投げる」。
  • try: 例外が発生する可能性のあるコードブロックを囲む。
  • catch: try ブロック内で投げられた例外を「捕まえて」処理する。

基本的な構文を見てみましょう。

exception_basic.cpp

divide 関数内で b が0だった場合に throw が実行され、関数の実行は即座に中断されます。制御は呼び出し元の catch ブロックに移り、そこでエラー処理が行われます。これにより、エラーが発生してもプログラム全体がクラッシュすることなく、安全に処理を続行できます。

例外とリソースリーク

ここで、newdelete を使った手動のメモリ管理と例外処理が組み合わさると、問題が発生します。

raw_pointer_problem.cpp

この例では、process_data 関数内で throw が実行されると、関数の実行が中断され catch ブロックにジャンプします。その結果、delete[] data; の行が実行されず、確保されたメモリが解放されないメモリリークが発生します。

この問題を解決するのが、C++の最も重要な設計思想の一つである RAII です。

RAIIイディオム

RAII (Resource Acquisition Is Initialization) は、「リソースの確保は、オブジェクトの初期化時に行い、リソースの解放は、オブジェクトの破棄時に行う」という設計パターンです。日本語では「リソース取得は初期化である」と訳されます。

C++では、オブジェクトがそのスコープ(変数が宣言された {} の範囲)を抜けるときに、そのオブジェクトのデストラクタが自動的に呼び出されます。この仕組みは、関数が正常に終了した場合だけでなく、例外が投げられてスコープを抜ける場合でも保証されています

RAIIはこの性質を利用して、リソースの解放処理をデストラクタに記述することで、リソースの解放を自動化し、delete の呼び忘れや例外発生時のリソースリークを防ぎます。

簡単なRAIIクラスの例を見てみましょう。

raii_concept.cpp

use_resource 関数が終了すると、rw オブジェクトがスコープを抜けるため、ResourceWrapper のデストラクタが自動的に呼び出され、delete[] が実行されます。もし use_resource の中で例外が発生したとしても、デストラクタは保証付きで呼び出されます。

この強力なRAIIイディオムを、動的メモリ管理のために標準ライブラリが提供してくれているのがスマートポインタです。

スマートポインタ: new/deleteを自動化する

スマートポインタは、RAIIを実装したクラステンプレートで、生ポインタ (int* など) のように振る舞いながら、リソース (確保したメモリ) の所有権を管理し、適切なタイミングで自動的に解放してくれます。

モダンC++では、メモリ管理に生ポインタを直接使うことはほとんどなく、スマートポインタを使うのが基本です。主に2種類のスマートポインタを使い分けます。

`std::unique_ptr`

std::unique_ptr は、管理するオブジェクトの所有権を唯一に保つスマートポインタです。つまり、あるオブジェクトを指す unique_ptr は、常に一つしか存在できません。

  • 唯一の所有権: コピーが禁止されています。オブジェクトの所有権を別の unique_ptr に移したい場合は、ムーブ (std::move) を使います。
  • 軽量: ポインタ一つ分のサイズしかなく、オーバーヘッドが非常に小さいです。

unique_ptr を作成するには、std::make_unique を使うのが安全で推奨されています。

unique_ptr_example.cpp

unique_ptr は、オブジェクトの所有者が誰であるかが明確な場合に最適です。基本的にはまず unique_ptr を使うことを検討しましょう。

`std::shared_ptr`

std::shared_ptr は、管理するオブジェクトの所有権を複数のポインタで共有できるスマートポインタです。

  • 共有された所有権: shared_ptr は自由にコピーできます。コピーされるたびに、内部の参照カウンタが増加します。
  • 自動解放: shared_ptr が破棄される(デストラクタが呼ばれる)と参照カウンタが減少し、参照カウンタが0になったときに、管理しているオブジェクトが解放(delete)されます。
  • オーバーヘッド: 参照カウンタを管理するための追加のメモリと処理が必要なため、unique_ptr よりもわずかにオーバーヘッドが大きいです。

shared_ptr を作成するには、std::make_shared を使うのが効率的で安全です。

shared_ptr_example.cpp

shared_ptr は、オブジェクトの寿命が単一のスコープや所有者に縛られず、複数のオブジェクトから共有される必要がある場合に便利です。ただし、所有権の関係が複雑になりがちなので、本当に共有が必要な場面に限定して使いましょう。

この章のまとめ

  • 例外処理try, catch, throw を使い、エラーが発生してもプログラムを安全に継続させるための仕組みです。
    • 手動のメモリ管理下で例外が発生すると、リソースリークを引き起こす危険があります。
    • RAIIイディオムは、リソースの確保をコンストラクタ、解放をデストラクタで行うことで、リソース管理を自動化するC++の重要な設計思想です。
    • スマートポインタはRAIIを動的メモリ管理に適用したもので、newdelete の手動管理を不要にします。
    • std::unique_ptr はオブジェクトの唯一の所有権を管理します。軽量であり、所有権が明確な場合に第一の選択肢となります。
    • std::shared_ptr はオブジェクトの所有権を共有します。参照カウントによって管理され、最後の所有者がいなくなったときにオブジェクトを解放します。

モダンC++プログラミングでは、newdelete を直接書くことは極力避け、RAIIとスマートポインタを全面的に活用することが、安全でメンテナンス性の高いコードへの第一歩です。

練習問題1: `unique_ptr` と所有権の移動

Employee という名前のクラスを作成してください。このクラスは、コンストラクタで社員名を受け取って表示し、デストラクタで「(社員名) is leaving.」というメッセージを表示します。

main 関数で、"Alice" という名前の Employee オブジェクトを std::make_unique で作成し、その unique_ptrpromote_employee という関数に渡してください。promote_employee 関数は unique_ptr を引数として受け取り(所有権が移動します)、「(社員名) has been promoted!」というメッセージを表示します。

プログラムを実行し、コンストラクタとデストラクタのメッセージが期待通りに表示されることを確認してください。

practice11_1.cpp

問題2: `shared_ptr` と所有権の共有

Project という名前のクラスを作成してください。コンストラクタでプロジェクト名を受け取り、デストラクタで「Project (プロジェクト名) is finished.」と表示します。

main 関数で、"Project Phoenix" という名前の Project オブジェクトを std::make_shared で作成してください。 次に、std::vector<std::shared_ptr<Project>> を作成し、作成した shared_ptr を2回 push_back してください。 その後、shared_ptr の参照カウント (use_count()) を表示してください。 最後に、vectorclear() して、再度参照カウントを表示してください。 プログラムの実行が終了するときに Project のデストラクタが呼ばれることを確認してください。

practice11_2.cpp