CPP04 1.0
|
C++98 で new
演算子などのエラーハンドリングにおいて、リソース解放を確実に行うための最も推奨される仕組みは、 RAII (Resource Acquisition Is Initialization) パターン です。
RAII は、「リソースの取得をオブジェクトの初期化と結びつけ、リソースの解放をそのオブジェクトのデストラクタと結びつける」という設計原則です。これにより、オブジェクトがスコープを抜けるとき(正常終了時でも、例外によってスタックが巻き戻される時でも)、デストラクタが自動的に呼び出され、リソースが確実に解放されます。
C++98 には std::unique_ptr
や std::shared_ptr
のようなモダンなスマートポインタはありませんが、 カスタムのスマートポインタ(RAII ラッパー)を自作する ことで同等の機能を実現できます。
ScopedPointer.hpp
(カスタムスマートポインタのヘッダーファイル) 2.このクラスは、
new
で単一のオブジェクトを割り当てる場合に特化した、簡易的なunique_ptr
のような振る舞いをします。コピーを禁止することで、所有権の曖昧さを避けます。#ifndef SCOPED_POINTER_HPP#define SCOPED_POINTER_HPP#include <iostream> // デバッグ出力用 (任意)// テンプレートクラスとして定義template <typename T>class ScopedPointer {private:T* m_ptr; // 管理する生ポインタ// コピーコンストラクタとコピー代入演算子を禁止// これにより、所有権のコピーを防ぎ、二重解放を回避するpublic:// コンストラクタ: 生ポインタを受け取り、所有権を持つexplicit ScopedPointer(T* ptr = 0) : m_ptr(ptr) {// std::cout << "ScopedPointer(" << m_ptr << ") created." << std::endl; // デバッグ用}// デストラクタ: スコープを抜けるときに管理下のメモリを解放~ScopedPointer() {// std::cout << "ScopedPointer(" << m_ptr << ") destroyed. Deleting resource." << std::endl; // デバッグ用delete m_ptr;}// 生ポインタへのアクセスを提供するT* get() const {return m_ptr;}// ポインタのように Dereference 演算子 (*) をオーバーロードT& operator*() const {// null ポインタの場合は未定義動作になるが、通常は呼び出し側でチェックreturn *m_ptr;}// ポインタのように Member Access 演算子 (->) をオーバーロードT* operator->() const {// null ポインタの場合は未定義動作になるが、通常は呼び出し側でチェックreturn m_ptr;}// 所有権を解放し、生ポインタを返す// ポインタを受け取った側が解放の責任を持つT* release() {T* temp = m_ptr;m_ptr = 0; // null にする (デストラクタで delete しないように)return temp;}// 新しいポインタを割り当て直す(以前のポインタは解放される)void reset(T* ptr = 0) {if (m_ptr != ptr) { // 同じポインタでないかチェックdelete m_ptr;m_ptr = ptr;}}};#endif // SCOPED_POINTER_HPPDefinition ScopedPointer.hpp:23
main.cpp
で ScopedPointer
を使用する
#include <iostream>#include <string>#include <new> // std::bad_alloc のために必要#include <cstdlib> // EXIT_FAILURE のために必要// 仮のクラス定義 (Animal, Dog, Cat など)// 実際には、これらのクラスのヘッダーファイルをインクルードしてくださいclass Animal {public:};public:};public:};// 作成したスマートポインタのヘッダーをインクルード#include "ScopedPointer.hpp"int main(){// ポインタ変数ではなく、ScopedPointer のインスタンスをスタックに作成する// これらは main 関数のスコープを抜けるときに自動的に解放されるScopedPointer<Animal> meta;try {// new で割り当てた生ポインタを ScopedPointer オブジェクトのコンストラクタに直接渡す// または reset() を使用する// もし C で例外がスローされたとしても、// A と B で割り当てられたメモリは、meta と j のデストラクタが// 自動的に呼ばれることで解放される。i->makeSound();j->makeSound();meta->makeSound();// ここで明示的に delete する必要はない// delete meta;// delete j;// delete i;// 例外捕捉後、ScopedPointer のデストラクタが自動で呼ばれ、// 既に割り当てたメモリが解放されることが保証される。return EXIT_FAILURE;return EXIT_FAILURE;} catch (...) {return EXIT_FAILURE;}// main 関数が終了するとき、meta, j, i の ScopedPointer オブジェクトがスコープを抜ける。// その際にそれぞれのデストラクタが呼ばれ、内部の生ポインタが指すメモリが解放される。return 0;}virtual void makeSound() constMakes a generic animal sound. This implementation is for the base Animal class. Derived classes are e...Definition Animal.cpp:66virtual void makeSound() constMakes the characteristic sound of a cat ("Meow!"). This function overrides the virtual makeSound() fr...Definition Cat.cpp:65virtual void makeSound() constMakes the characteristic sound of a dog ("Woof!"). This function overrides the virtual makeSound() fr...Definition Dog.cpp:65T endl(T... args)T what(T... args)
new
で割り当てたもの) は、 ScopedPointer オブジェクトが構築されるときに「取得」されます。delete
演算子によってヒープメモリが「解放」されます。new
が返した生ポインタを受け取ります。delete
を呼び出します。private
にすることで、 ScopedPointer オブジェクトのコピーを禁止しています。これにより、所有権は常に単一の ScopedPointer オブジェクトに限定されます。operator*()
と operator->()
をオーバーロードしているため、 ScopedPointer オブジェクトをまるで生ポインタのように *ptr
や ptr->member
の形で使うことができます。reset()
メソッド: 必要に応じて、 ScopedPointer が管理するポインタを別のポインタに置き換えたり、0
にしたりできます。reset()
が呼ばれると、それまで管理していたメモリは解放されます。try
ブロック内で new Cat();
が std::bad_alloc
をスローした場合、その時点ですぐに try
ブロックを抜け、対応する catch
ブロックにジャンプします。meta
と j
は ScopedPointer のオブジェクトとしてスタックに存在しているため、スコープを抜ける際にそれぞれのデストラクタが自動的に呼び出されます。meta
と j
が指していたメモリは正しく解放され、メモリリークは発生しません。C++98 で new
のエラーハンドリングにおいて、メモリリークを防ぐための最も堅牢で推奨される方法は、このように RAII パターンを適用したカスタムのスマートポインタを実装することです。