O'Reilly Early Releaseで購入したEffective Modern C++の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)
Chapter4: スマートポインタ
生ポインタを愛し難い理由
- ポインタ宣言からはそれが1つのオブジェクトを指すのか配列を指すのか分からない
- ポインタ宣言からはそれを使い終わった時にその指し先を破棄すべきなのか分からない
- ポインタを破棄する時にdeleteを呼べば良いのかそれとも他の破棄の仕組みがあるのか(専用の関数があるなど)が分からない
- deleteとdelete[]のどちらを呼ぶ必要があるのか分からない。間違った選択をすると未定義動作に
- 破棄の仕方が特定できていたとしても、1回だけ破棄を呼び出すことを全てのパス(例外発生の場合も含む)において確かにするのは難しい。もし呼び出されなかった場合はリソースがリークし、1回より多く呼び出されたら未定義動作になる
- ポインタの指し先が既に削除されているかどうか(dangling pointerかどうか)判定する方法は無い。
生ポインタは強力なツールだが、どんなに集中してもどんなに鍛えられていてもほんの些細な過ちですぐにポインタはおかしくなってしまう。
スマートポインタがこれらの問題を解決する1つの方法であり、生ポインタよりもスマートポインタを優先して使うべきだ。
4つのスマートポインタ
- std::auto_ptr
- std::unique_ptr
- std::shared_ptr
- std::weak_ptr
std::auto_ptrはdeprecatedとなったC++98の異物である。
C++98にはムーブセマンティクスが無いため、std::auto_ptrはコピーした時にコピー元がnullになったりコンテナに入れられなかったりなどの問題がある。
C++11ではstd::unique_ptrはstd::auto_ptrにできることが全てできる上に実行効率も良い。
従ってstd::auto_ptrを使う唯一の場面はC++98でコンパイルする必要のある時のみであり、そうでなければ常にunique_ptrの方を使うべき。
スマートポインタのAPIは非常に多様で、唯一全てに共通するものはデフォルトコンストラクタであり、網羅的説明はしない。
網羅的説明は他所にあるから、ここではAPIの概要には書かれないようなスマートポインタを効果的に使う方法に集中する。
Item 18: 排他的な所有権を表現するリソース管理にはstd::unique_ptrを使おう
スマートポインタで最も手近に置いておくべきなのがstd::unique_ptrである。
生ポインタと同じメモリサイズであり生ポインタのほとんど全ての演算を実行でき、全く同じ命令を実行する。
従って実行速度とメモリ容量が非常に厳しい環境においてもstd::unique_ptrを生ポインタと同等に使用できる。
std::unique_ptrは排他的な所有権を持つ。
nullでないstd::unique_ptrは指し先の実体を必ず所有していて、std::unique_ptrをmoveすると所有権が移る。
排他的所有権なのでstd::unique_ptrのコピーは不可。
std::unique_ptrはmove専用の型。
std::unique_ptrのデフォルトデストラクタでは中の生ポインタにdeleteを適用する。
ファクトリ関数での利用
std::unique_ptrのありふれたユースケースはファクトリ関数。
class Investment { … }; class Stock: public Investment { … }; class Bond: public Investment { … }; class RealEstate: public Investment { … };
ファクトリ関数
template<typename... Ts> std::unique_ptr<Investment> makeInvestment(Ts&&... params);
呼び出し
{ auto pInvestment = makeInvestment( arguments ); … }
カスタムデリータ
std::unique_ptrにはコンストラクタでカスタムデリータを指定できる。
カスタムデリータは任意の関数もしくはラムダ式を含む関数オブジェクトに対応。
auto delInvmt = [](Investment* pInvestment) { makeLogEntry(pInvestment); delete pInvestment; } template<typename... Ts> std::unique_ptr<Investment, decltype(delInvmt)> // C++14ならautoで良い makeInvestment(Ts&&... params) { std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); if (/* Stockオブジェクトを作る場合 */) { pInv.reset(new Stock(std::forward<Ts>(params)...)); } else if (/* Bondオブジェクトを作る場合) { pInv.reset(new Bond(std::forward<Ts>(params)...)); } … return pInv; }
- Investmentポインタ型を破棄するdelInvmtをラムダ式で生成(後述の理由でラムダ式の方が実行効率が良い)
- カスタムデリータの型をstd::unique_ptrのテンプレート第二引数に指定
- カスタムデリータオブジェクトをstd::unique_ptrのコンストラクタ第二引数に与える
- std::unique_ptrに生ポインタを代入することはできない(生ポインタからスマートポインタへの暗黙の型変換は危険なため)。生ポインタの代入にはresetを用いる。
- std::forwardを用いている理由はitem25で説明
- カスタムデリータはInvestmentポインタを引数として取ってそれをdeleteしているので、Investmentクラスのデストラクタはvirtual指定すること
std::unique_ptrのメモリサイズ
デフォルトデリータを使う場合は生ポインタと同一サイズ。
カスタムデリータを使う場合、関数ポインタならポインタのサイズ分、関数オブジェクトならその内部状態のメモリが加わる。
関数オブジェクトがステートレス(メンバ変数を持たない)であればサイズは増加しない。
したがって、ステートレスなカスタムデリータはポインタ分サイズが増える関数よりもラムダ式を指定した方が良い。
std::unique_ptrはPimplイディオムでも使用されるが詳細はItem 22を参照。
std::unique_ptrではオブジェクトと配列を区別できる
std::unique_ptr<T>型には[]演算子は無く、std::unique_ptr<T[]>型には*と->演算子は無いなどオブジェクトのunique_ptrと配列のunique_ptrは特殊化されている。
しかしC言語ライクなAPIで配列をヒープに入れて返すようなインターフェース以外ではstd::unique_ptr<T[]>型は普通使わない。
ポインタの配列よりもstd::array, std::vector, std::stringを使用する方が良いため。
std::unique_ptrは容易にshared_ptrへ変換可能
下のコードのようにstd::unique_ptrからstd::shared_ptrへの変換は容易なので、ファクトリ関数はunique_ptrを返しておけば呼び出し側でどちらのポインタでも扱える。
std::shared_ptr<Investment> sp = makeInvestment( arguments );
std::shared_ptrの詳細についてはItem 19を参照。