2016年2月18日木曜日

【読書ノート】Effective Modern C++ - Item 21

O'Reilly Early Releaseで購入したEffective Modern C++の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 21: 直接newを呼び出すよりもstd::make_uniqueとstd::make_sharedを使おう

std::make_sharedはC++11からあるが、std::make_uniqueはC++14から。
C++11の場合は自分で以下のコードで基本的なstd::make_uniqueを用意できる。

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
  return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

make_uniqueはパラメータをそのままperfect-forwardでコンストラクタに渡し、newでオブジェクトを作ってunique_ptrを返すだけ。
C++14からはstd名前空間に登場するから、自分で作る場合はstd以外の名前空間に置くように。

make関数には3種類あって、残りの1つはstd::allocate_shared。
第一引数に動的メモリ割り当てのためのアロケータオブジェクトを渡すことができる。

make関数を使うべき理由1: クラス名の指定の重複を避ける

auto spw1(std::make_shared<Widget>());
std::shared_ptr<Widget> spw2(new Widget);

newを使うと2回Widgetというクラス名を指定することになり、以下の欠点が生じる。

  • ソースコードの重複はコンパイル時間の増大やコード肥大を招く可能性がある
  • 重複は一貫性の無いコードになりがちで、バグの原因になる
  • 同じものを2回タイプすることは無駄な負担である

make関数を使うべき理由2: メモリリークを防ぐ

processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

複数ある関数引数が与えられた順に実行されるとは限らない。
computePriorityが呼び出されるのはnew Widgetの前かもしれないし、std::shared_ptr生成の後かもしれないし、その「間」かもしれない
1. new Widgetが実行される
2. computePriorityが呼ばれる
3. std::shared_ptrコンストラクタが呼ばれる
もし2の段階で例外が発生した場合は3に辿りつけず、メモリリークが発生する。
以下のようにstd::make_sharedを使えば回避できる。

processWidget(std::make_shared<Widget>(), computePriority());

make関数を使うべき理由3: コードの最適化

以下のコードではnew Widgetとshared_ptrコンストラクタでそれぞれ別々にメモリアロケーションが発生する。
前者はWidgetのオブジェクトそのもの、後者はリファレンスカウントを含むコントロールブロック用のメモリアロケーションを行う。

std::shared_ptr<Widget> spw(new Widget);

一方、std::make_sharedを使用すればメモリアロケーションはWidgetオブジェクトとコントロールブロック用のメモリは1つの領域に同時に確保される。

auto spw = std::make_shared<Widget>();

そのためコンパイラで生成されるコードは短くなるし実行速度も上がる。
std::allocate_sharedも同様。

make関数を使えないケース

カスタムデリータを指定したい時

カスタムデリータを指定したい時は直接newを使うしか無い。

auto widgetDeleter = [](Widget* pw) { … };
std::unique_ptr<Widget, decltype(WidgetDeleter)> upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);

make関数を使えないのはstd::unique_ptrは上記のケースだけだが、std::shared_ptrにはもう2つ注意すべき点がある。

非常に大きなオブジェクトを扱う時

std::shared_ptrのコントロールブロックには通常のリファレンスカウントの他にもう1つのリファレンスカウントであるweak countが存在する。
これはいくつのweak_ptrがコントロールブロックを参照しているかを表す。
weak_ptrはリファレンスカウントを見てexpiredかどうかを判定する。
したがって、コントロールブロックを見ているweak_ptrが存在する限りはコントロールブロックを破棄できない。
しかしstd::make_sharedで生成されたコントロールブロックとオブジェクトは同じメモリ領域に割り当てられている。
つまりリファレンスカウントが0になってもweak countも0になる=全てのweak_ptrが破棄されるまでオブジェクト用に確保されたメモリが解放されない。
オブジェクトが消費するメモリが大きな場合は問題となり得る。
newを使った場合はリファレンスカウントが0になった時点でメモリは解放されるため、この場合はstd::make_sharedではなくnewを使った方が良いということになる。

関数引数でカスタムデリータを使う時

以下のコードはcomputePriorityが例外を発生するとメモリリークの可能性がある。

void cusDel(Widget *ptr);

processWidget(
  std::shared_ptr<Widget>(new Widget, cusDel),
  computePriority()
);

以下のようにstd::shared_ptrの生成を分離すればメモリリークは回避できるがstd::shared_ptrのコピーが発生する。
std::shared_ptrのコピーはレファレンスカウントのアトミックな操作が発生するためオーバーヘッドが無視できない。

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority());

std::moveを使えばこの問題を解決できる。

std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(std::move(spw), computePriority());