これまでの章で、new
と delete
を使った動的なメモリ管理を学びました。しかし、これらの手動管理は delete
の呼び忘れによるメモリリークや、複雑なコードでのリソース管理の煩雑さを引き起こす原因となりがちです。
C++11以降の「モダンC++」では、こうした問題を解決するための洗練された仕組みが導入されました。この章では、エラーハンドリングのための例外処理、リソース管理の基本思想である RAIIイディオム、そしてそれを具現化するスマートポインタ (std::unique_ptr
, std::shared_ptr
) について学び、より安全で堅牢なコードを書くための流儀を身につけます。
プログラムでは、ファイルの読み込み失敗やメモリ確保の失敗など、予期せぬエラーが発生することがあります。C++では、このような状況を処理するために例外 (Exception) という仕組みが用意されています。
例外処理は、以下の3つのキーワードで構成されます。
throw
: 例外的な状況が発生したことを知らせるために、例外オブジェクトを「投げる」。try
: 例外が発生する可能性のあるコードブロックを囲む。catch
: try
ブロック内で投げられた例外を「捕まえて」処理する。基本的な構文を見てみましょう。
divide
関数内で b
が0だった場合に throw
が実行され、関数の実行は即座に中断されます。制御は呼び出し元の catch
ブロックに移り、そこでエラー処理が行われます。これにより、エラーが発生してもプログラム全体がクラッシュすることなく、安全に処理を続行できます。
ここで、new
と delete
を使った手動のメモリ管理と例外処理が組み合わさると、問題が発生します。
この例では、process_data
関数内で throw
が実行されると、関数の実行が中断され catch
ブロックにジャンプします。その結果、delete[] data;
の行が実行されず、確保されたメモリが解放されないメモリリークが発生します。
この問題を解決するのが、C++の最も重要な設計思想の一つである RAII です。
RAII (Resource Acquisition Is Initialization) は、「リソースの確保は、オブジェクトの初期化時に行い、リソースの解放は、オブジェクトの破棄時に行う」という設計パターンです。日本語では「リソース取得は初期化である」と訳されます。
C++では、オブジェクトがそのスコープ(変数が宣言された {}
の範囲)を抜けるときに、そのオブジェクトのデストラクタが自動的に呼び出されます。この仕組みは、関数が正常に終了した場合だけでなく、例外が投げられてスコープを抜ける場合でも保証されています。
RAIIはこの性質を利用して、リソースの解放処理をデストラクタに記述することで、リソースの解放を自動化し、delete
の呼び忘れや例外発生時のリソースリークを防ぎます。
簡単なRAIIクラスの例を見てみましょう。
use_resource
関数が終了すると、rw
オブジェクトがスコープを抜けるため、ResourceWrapper
のデストラクタが自動的に呼び出され、delete[]
が実行されます。もし use_resource
の中で例外が発生したとしても、デストラクタは保証付きで呼び出されます。
この強力なRAIIイディオムを、動的メモリ管理のために標準ライブラリが提供してくれているのがスマートポインタです。
スマートポインタは、RAIIを実装したクラステンプレートで、生ポインタ (int*
など) のように振る舞いながら、リソース (確保したメモリ) の所有権を管理し、適切なタイミングで自動的に解放してくれます。
モダンC++では、メモリ管理に生ポインタを直接使うことはほとんどなく、スマートポインタを使うのが基本です。主に2種類のスマートポインタを使い分けます。
std::unique_ptr
は、管理するオブジェクトの所有権を唯一に保つスマートポインタです。つまり、あるオブジェクトを指す unique_ptr
は、常に一つしか存在できません。
unique_ptr
に移したい場合は、ムーブ (std::move
) を使います。unique_ptr
を作成するには、std::make_unique
を使うのが安全で推奨されています。
unique_ptr
は、オブジェクトの所有者が誰であるかが明確な場合に最適です。基本的にはまず unique_ptr
を使うことを検討しましょう。
std::shared_ptr
は、管理するオブジェクトの所有権を複数のポインタで共有できるスマートポインタです。
shared_ptr
は自由にコピーできます。コピーされるたびに、内部の参照カウンタが増加します。shared_ptr
が破棄される(デストラクタが呼ばれる)と参照カウンタが減少し、参照カウンタが0になったときに、管理しているオブジェクトが解放(delete
)されます。unique_ptr
よりもわずかにオーバーヘッドが大きいです。shared_ptr
を作成するには、std::make_shared
を使うのが効率的で安全です。
shared_ptr
は、オブジェクトの寿命が単一のスコープや所有者に縛られず、複数のオブジェクトから共有される必要がある場合に便利です。ただし、所有権の関係が複雑になりがちなので、本当に共有が必要な場面に限定して使いましょう。
try
, catch
, throw
を使い、エラーが発生してもプログラムを安全に継続させるための仕組みです。
new
と delete
の手動管理を不要にします。std::unique_ptr
はオブジェクトの唯一の所有権を管理します。軽量であり、所有権が明確な場合に第一の選択肢となります。std::shared_ptr
はオブジェクトの所有権を共有します。参照カウントによって管理され、最後の所有者がいなくなったときにオブジェクトを解放します。モダンC++プログラミングでは、new
と delete
を直接書くことは極力避け、RAIIとスマートポインタを全面的に活用することが、安全でメンテナンス性の高いコードへの第一歩です。
Employee
という名前のクラスを作成してください。このクラスは、コンストラクタで社員名を受け取って表示し、デストラクタで「(社員名) is leaving.」というメッセージを表示します。
main
関数で、"Alice"
という名前の Employee
オブジェクトを std::make_unique
で作成し、その unique_ptr
を promote_employee
という関数に渡してください。promote_employee
関数は unique_ptr
を引数として受け取り(所有権が移動します)、「(社員名) has been promoted!」というメッセージを表示します。
プログラムを実行し、コンストラクタとデストラクタのメッセージが期待通りに表示されることを確認してください。
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()
) を表示してください。
最後に、vector
を clear()
して、再度参照カウントを表示してください。
プログラムの実行が終了するときに Project
のデストラクタが呼ばれることを確認してください。