2016年11月1日火曜日

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

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

Item 29: ムーブ操作が存在せず、低コストでなく、使われない場合を前提としよう

C++11のムーブセマンティクスは、高コストなコピー操作を置き換え、ただ単にC++98のコードをC++11でリビルドするだけで早く動くようになるという伝説的な特徴だ。
しかし伝説は誇張されるものであり、過度な期待を持つべきではない。

C++98の標準ライブラリはC++11のムーブ操作を使ってコピー操作より速くなるように総整備された。
しかしC++11向けに改修されていないユーザー定義のクラスやライブラリは何の恩恵も得られないことが多い。
ムーブ操作はクラス内に定義されていなくてもコンパイラによって自動生成されるが、それは一切のコピー操作・ムーブ操作・デストラクタが定義されていない場合に限る。(item 17)
また、ムーブ操作がdeleteで無効化されているデータメンバを持つ場合も自動生成されない。

標準ライブラリのコンテナにしても、内容を低コストでムーブすることができない、またはコンテナ内の要素がコンテナのムーブ操作に対応していないなどの理由でムーブ操作の恩恵を受けられないことがある。
たとえばstd::arrayはSTLのインターフェースが定義された組み込み配列だが、これはデータをヒープ領域にポインタで持つ他のコンテナがポインタのコピーという定数時間の処理でムーブ操作可能であることと異なる。

std::vector<Widget> vw1;
…
// 単にvw1とvw2のポインタを書き換えるだけでムーブ完了(定数時間)
auto vw2 = std::move(vw1);

std::array<Widget, 10000> aw1;
…
// aw1の全ての要素をaw2にムーブする(線形時間)
auto aw2 = std::move(aw1);

Widgetクラスのムーブ操作が高速である場合、std::arrayの場合も確かにコピーよりも速くなるが、n個の要素それぞれをムーブするので定数時間でムーブが完了するstd::vectorと比べると高速化のインパクトはずっと小さい。

std::stringは定数時間のムーブ操作と線形時間のコピーを提供しているのでムーブがコピーより速いように思える。
しかし15文字以下の小さい文字列の場合はSSO (small string optimization) が適用されるため当てはまらない。
なぜなら小さい文字列の場合はヒープ領域ではなくstd::stringオブジェクト内のバッファ領域にストアされるため。
この場合、ムーブ操作はコピー操作と同じ時間がかかる。
SSOが存在するのは実際のアプリケーションでは短い文字列が多用され、その場合動的にメモリ領域を確保するのは高コストであるから。

せっかく高速なムーブ操作が実装されているクラスでも、noexcept指定されていないとムーブが呼び出されない場合もある。
item 14で書かれているようにコンテナの中には強い例外安全性を要求する操作があり、そこで呼び出されるムーブ操作がnoexcept指定されていない場合、コピー操作が呼ばれる。(ムーブ操作で置き換えてくれない。)

まとめると以下の場合ムーブ操作の恩恵を受けられない。

  • ムーブされるオブジェクトでムーブ操作が定義されていない場合
  • ムーブされるオブジェクトのムーブ操作がコピー操作よりも速くない場合
  • 例外の発生が許されないコンテキストにおいてムーブ操作がnoexceptで宣言されていない場合
  • ムーブされるオブジェクトがlvalueである時 (std::moveはlvalueをrvalueにキャストしている)

このitemのタイトルで、ムーブ操作が存在せず、低コストでなく、使われない場合を「前提としよう」としたのは、ジェネリックなコードを書く場合にはそうすべきだからである。
たとえばtemplateを使っている場合はどんなクラスが与えられるのか分からない。
もちろんどのようなクラスがそのtemplateを使うのか分かっている場合は前提とする必要はない。
たとえばどのクラスも高速なムーブ操作が提供されていると分かっているのならば、コピー操作をムーブ操作に置き換えることでムーブセマンティクスの恩恵を受けることができる。

2016年8月25日木曜日

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

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

Item 37: 全てのパスでstd::threadがunjoinableとなるようにすること

joinableなstd::threadのデストラクタが呼ばれると強制終了される

std::threadインスタンスは"joinable"と"unjoinable"の2つのいずれかの状態にある。
joinableは、スレッドが実行されているかもしくは実行可能な状態。たとえばブロックされていたり、待っていたり、実行完了したスレッドが該当する。
unjoinableは、joinableでないスレッドのことで、以下の場合を含む。

  • デフォルトコンストラクタで初期化されたstd::thread
  • moveされた後のstd::thread
  • 既にjoinされたstd::thread
  • detachされたstd::thread

std::threadがjoinableであるかどうかが重要な理由の1つは、joinableなstd::threadのデストラクタが呼ばれるとプログラムがterminateされること。
たとえば以下のプログラムではif文に入らないか例外が発生した時にt.join()が呼ばれずプログラムがterminateされる。

constexpr auto tenMillion = 10000000;

bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion)
{
  std::vector<int> goodVals;
  std::thread t([&filter, maxVal, &goodVals] {
    for (auto i = 0; i <= maxVal; ++i) {
      if (filter(i)) goodVals.push_back(i);
    }
  });
  auto nh = t.native_handle();
  … // tの優先度を設定
  if (conditionsAreSatisfied()) {
    t.join();
    performComputation(goodVals);
    return true;
  }
  return false;
} 

なお、通常はitem 35で学習したtask-based設計 (std::asyncを使うやり方) を採用した方が良いがここではスレッドの優先度を指定するためstd::threadを利用してnative_handleを呼び出している。
ただし優先度は本来std::threadの実行前、サスペンド状態のまま設定した方が良いが、これについてはitem 39で扱う。
ちなみにC++14では以下のように数値を表記できる。

constexpr auto tenMillion = 10'000'000;

話を戻し、joinableなstd::threadのデストラクタが呼ばれると、なぜプログラムがterminateされてしまうようになっているのか?
それを考えるにはそうしない2つのケースについて考えると良い。

暗黙的にjoinする場合

これはstd::threadのデストラクタがスレッドの実行が完了するのを待つことを意味する。
そうなるとやっかいなパフォーマンス問題が生じる可能性がある。
上記のプログラムの場合、conditionAreSatisfied()がfalseを返しているにも関わらずフィルタ処理を行っていることとなる。
これは明らかにプログラマの意図に反する。

暗黙的にdetachする場合

この場合、std::threadインスタンスとスレッド実行が切り離され、スレッドはバックグラウンドで実行され続ける。
これは一見暗黙的なjoinよりかはマシに見えるが、デバッグ上の問題でより悪くなる可能性がある。
たとえばスレッドに渡しているラムダ式ではgoodValsを参照キャプチャしているが、これはローカル変数なのでdoWorkを抜けると使用できなくなる。
具体的には、doWorkの呼び出し元が別の関数を呼び出し、doWork内のgoodValsのメモリ領域を再利用するとgoodValはもう使えなくなる。
この問題が生じた場合のデバッグの困難さは容易に想像できる。

以上の理由により、標準化委員会はjoinableなstd::threadを破棄することを禁止した。
つまり、プログラムをterminateさせるようにした。

RAIIなthreadクラスによる強制終了の回避

しかし実際のところ全てのpathでunjoinableになることを確認することは重荷である。
return, continue, goto, それに例外などスコープを抜けてしまう要因は多い。
それに対する通常の対処法はローカルオブジェクトのデストラクタを利用すること、つまりRAII (Resource Acquisition Is Initialization) オブジェクトを利用することである。
表記に使われているのはinitializationではあるが重要なのはdestruction。
例としてはSTLコンテナ、スマートポインタ、std::fstreamなど。
しかし標準化委員会としてはstd::threadをデストラクタでどうすべきなのかは分からないのでjoinもdetachもデストラクタとして採用されていない。
ただし自分でstd::threadをRAIIクラスに書くことは容易である。

class ThreadRAII {
public:
  enum class DtorAction {join, detach};

ThreadRAII(std::thread&& t, DtorAction a)
  : action(a), t(std::move(t)) {}

~ThreadRAII() {
  if (t.joinable()) {
    if (action == DtorAction::join) {
      t.join();
    } else {
      t.detach();
    }
  }
}

std::thread& get() {return t;}

private:
  DtorAction action;
  std::thread t;
};

コンストラクタでrvalueを取っているのはstd::threadがnoncopyableであるため。

std::threadは初期化される時に実行が始まるため、他のデータメンバの初期化を先に済ませるため、std::threadはデータメンバの中で最後に宣言すると良い。

get関数を用意し必要に応じてstd::threadを取り出せるようにしている。
それによってstd::threadのインターフェースを全て実装せずに済んでいる。

unjoinableなstd::threadに対してjoinまたはdetachをすると未定義動作となるので、事前にt.joinable()でチェックしている。
たとえばgetによって取得されたstd::threadがmoveやjoinやdetachが呼ばれている場合はunjoinableになっている。
また、別のスレッドがメンバ関数を呼ぶことでデストラクタと同時に処理が進む場合、t.joinable()を読んだ時はjoinableで、その後のt.join() / t.detach()を呼ぶ時にunjoinableになるという競合があり得る。
しかし一般的には1つのオブジェクトに対してスレッドセーフに呼び出せるのはconstメンバ関数に限られる (item 16参照) ので、これを守っている限りは生じない競合である。

このThreadRAIIクラスを利用して冒頭のプログラムを記述すると以下のようになる。

bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion)
{
  std::vector<int> goodVals;
  ThreadRAII t(std::thread([&filter, maxVal, &goodVals] {
      for (auto i = 0; i <= maxVal; ++i) {
        if (filter(i)) goodVals.push_back(i);
      }
    }), ThreadRAII::DtorAction::join);
  auto nh = t.get().native_handle();
  …
  if (conditionsAreSatisfied()) {
    t.get().join();
    performComputation(goodVals);
    return true;
  }
  return false;
}

ここではパフォーマンス上のやっかいな問題を受容してThreadRAIIのデストラクタオプションでjoinを選択している。
未定義動作とプログラムの強制終了に比べるとマシだという判断である。
ただしItem 39で示すがjoinするやり方はプログラムがhungする(フリーズする)ケースも存在し得る。
それに対する適切な解法はもう処理を続けるひつようの無いlambda式に対してすぐに終了するよう伝達することであるが、このようなinterruptible threadはstd::threadはC++11ではサポートしておらず、自分で書く必要があるがそれは本書のスコープ外とする。
(Anthony Williamsの"C++ Concurrency in Action" (Manning Publications, 2012)のsection 9.2.に良いやり方が載っている。)

Item 17で説明したように、ThreadRAIIではデストラクタが宣言されているためmove操作はコンパイラで自動生成されない。
ThreadRAIIがmoveされてはならない理由はないので、自動生成をリクエストする場合は以下の記述を加えれば良い。

class ThreadRAII {
  …
  ThreadRAII(ThreadRAII&&) = default; // support
  ThreadRAII& operator=(ThreadRAII&&) = default; // moving
  … 
};

2016年7月7日木曜日

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

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

Item 36: 非同期性が重要な場合はstd::launch::asyncを指定しよう

std::launch::async => fは異なるスレッドで非同期に実行される
std::launch::deferred => fはstd::asyncで返されたfutureのgetかwaitが呼ばれた時にだけ実行される。getかwaitが呼ばれるとfは同期的に実行される。
デフォルトは両者のor、つまり以下の2つの文は同一の意味を持つ。

auto fut1 = std::async(f);
auto fut2 = std::async(std::launch::async | std::launch::deferred, f);

このデフォルト設定の場合、item 35で書かれている通り標準ライブラリに適切なスレッドの管理を任せることができる。
この場合の注意点は以下の3つ。
(auto fut = std::async(f);が実行されるスレッドをtとする。)

  • fがtと並行に(concurrentlyに)実行されるかどうか予測することはできない (fの実行がdeferredされる可能性があるため)
  • fがfutに対してgetかwaitを呼ぶスレッドと異なるスレッドで実行されるかどうか予測することはできない
  • fが実行されるかどうか予測できない場合がある(futに対してgetかwaitが呼ばれないpathがあるかもしれないため)

デフォルトのlaunch policyで注意する必要があるのはスレッドローカル変数 (thread_localで宣言する変数) を利用する場合。
なぜならfが読み込むスレッドローカルストレージ (TLS) がどのスレッドのものなのか予測できないため。

以下のコードは終了しない可能性がある。

using namespace std::literals;
void f() {
  std::this_thread::sleep_for(1s);
}
auto fut = std::async(f);
while(fut.wait_for(100ms) != std::future_status::ready) {
  …
}

fがdeferredされている場合、std::future_status::deferredが返されるため、std::future_status::readyと==にならない。
この種のバグは高負荷な状況でなければ明らかにならないため、開発やユニットテストでは容易に見逃してしまう。
それはoversubscription (ハードウェアのスレッド数を超えるソフトウェアスレッドがある状況) とスレッドの枯渇が生じた場合にfの実行の延期 (deferred) が生じるため。

この問題を回避するにはwait_forでdeferredが返ってくるかどうかを確かめれば良い。
タイムアウトを0に設定すれば即時判定が可能。

auto fut = std::async(f);
if (fut.wait_for(0s) == std::future_status::deferred) {
  … // futにwaitかgetを使用してfを同期的に呼び出す
}
else {
  while(fut.wait_for(100ms) != std::future_status::ready) {
    … // fは並行処理されているのでそれが完了するのを待つ
  }
  … // この時点でfutはreadyに
}

結論として、デフォルトのlaunch policyを使用して良いのは以下の条件が満たされる場合である。

  • タスクはgetまたはwaitを呼ぶスレッドと並行に実行される必要は無い
  • どのスレッドのthread_local変数が読み書きされても問題にならない
  • std::asyncが返すfutureに対してgetまたはwaitが呼ばれる保証があるか、もしくはタスクがずっと実行されない場合でも問題ない
  • wait_forまたはwait_untilを使用するコードではdeferred statusになっている可能性を考慮している

上記の条件のうち1つでも満たされない場合はstd::lanuch::asyncを指定する。

auto fut = std::async(std::launch::async, f);

std::launch::asyncを自動的に付加するために以下のようなユーザー定義関数を用意するのも良い手。

// C++11 version
template <typename F, typename... Ts>
inline std::future<typename std::result_of<F(Ts...)>::type>
reallyAsync(F&& f, Ts&&... params) {
  return std::async(std::launch::async,
                    std::forward<F>(f),
                    std::forward<Ts>(params)...);
}
// C++14 version
template <typename F, typename... Ts>
inline auto
reallyAsync(F&& f, Ts&&... params) {
  return std::async(std::launch::async,
                    std::forward<F>(f),
                    std::forward<Ts>(params)...);
}

2016年4月14日木曜日

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

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

Item 25: rvalue referenceにはstd::moveを、universal referenceにはstd::forwardを使おう

std::moveとstd::forwardの使い分け

rvalue referenceは無条件にmoveして良い。
std::forwardを使っても正しい動作をするが、不自然な表現なので避けるべき。

class Widget {
public:
  Widget(Widget&& rhs) : name(std::move(rhs.name)) {…}
private:
  std::string name;
}

universal referenceの場合はrvalueで初期化された時だけrvalueにcastされなければならないので、(item23で説明した)std::forwardを使う。

class Widget {
public:
  template<typename T>
  void setName(T&& newName) {
    name = std::forward<T>(newName);
  }
  …
}

universal referenceにstd::moveを使用してはならない。

class Widget {
public:
  template<typename T>
  void setName(T&& newName) {
    name = std::move<T>(newName);
  }
  …
}
Widget w;
auto n = getWidgetName();
w.setName(n);

nはlvalueなので、w.setName(n)から戻って来た後、nはunspecified valueになってしまう。

universal referenceの優位性

以下の様な主張を言う人がいるかもしれない。
universal referenceはconstでは無いから、setNameのように変数の値を変化させない関数ではconst参照とrvalue referenceでオーバーロードするべきだと。

class Widget {
public:
  void setName(const std::string& newName)
  { name = newName; }
  void setName(std::string&& newName)
  { name = std::move(newName); }
  …
};

しかしこれは良くない。
まず、オーバーロードバージョンではw.setName("Adela Novak");とするとstd::stringの一時オブジェクトが生成されてしまい、オーバーヘッドが生じる。
std::stringのコンストラクタが呼ばれてsetNameのrvalue referenceにバインドされ、moveされ、デストラクタが呼ばれる。
universal referenceを使うバージョンでは文字列リテラルがそのまま渡されるから、std::stringの一時オブジェクトは生成されない。
次に、universal referenceを使うバージョンでは1つだった関数が2つに増えてメンテナンス性が悪くなっている。
これは引数が増えるほど問題が悪化する。引数毎にオーバーロードするなら2のべき乗で関数が増えていってしまう。
そして可変個引数の場合は不可能になり、universal referenceを使うしか無い。

template<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);

関数内で複数回universal referenceを使う場合は最後の文にだけstd::forwardを使う必要がある。(rvalue referenceの場合はstd::move。)

template<typename T>
void setSignText(T&& text)
{
  sign.setText(text); // 渡し先でtextを壊されないようにrvalueとして渡さない
  auto now = std::chrono::system_clock::now();
  signHistory.add(now, std::forward<T>(text));
}

rvalue/universal referenceで受け取った引数を値渡しでreturnする場合

rvalue referenceで受け取った引数を値渡しでreturnする場合はstd::moveするべき。

Matrix operator+(Matrix&& lhs, const Matrix& rhs)
{
  lhs += rhs;
  return std::move(lhs);
}

lhsをmoveしないと必ずコピーが発生してしまう。
Matrixがムーブコンストラクタをサポートしている場合はstd::moveを利用することで高速化できる。
また、Matrixがムーブコンストラクタをサポートしていなくてもstd::moveの恩恵を受けられずコピーが発生するだけで動作に問題は起こらない。
従って値渡しする場合は無条件にstd::moveで返しておけば良い。

同じく値渡しでreturnする関数でuniversal referenceが引数の場合はstd::forwardで返しておけば良い。

template<typename T>
Fraction reduceAndCopy(T&& frac)
{
  frac.reduce();
  return std::forward<T>(frac);
}

rvalue referenceの場合は先ほどの例と同様で無駄なコピーを回避し、lvalue referenceの場合はコピーして返す。

RVOが適用される場合はstd::move/forwardを使ってはならない

戻り値が値渡しである関数で、戻り値にするローカル変数と戻り値の型が同じである場合はreturn value optimizatinによりコピーされることはない。
したがってRVOの条件を満たす場合はローカル変数をreturnする時にstd::moveやstd::forwardを適用しないようにする。

Widget makeWidget()
{
  Widget w;
  …
  return w; // std::moveをつけないようにする
} 

本当にRVOされるか不安な人はコピーを避けるためstd::moveを付けてしまうかもしれない。
しかしその場合はRVOは働かなくなる。std::moveはrvalue "reference"であるから戻り値の型とは異なる型となる。
結果としてムーブコンストラクタが呼ばれる分だけオーバーヘッドが生じる。
それでも確実にコピーを避けられるという考えが浮かぶかもしれないがそれは誤り。
コンパイラは、RVOが効かない場合は自動的にstd::moveを付加してreturnするようになっている。
従って戻り値の型と同一の型であるローカル変数をreturnする時にstd::moveを行うことは全く無意味であり、やってはならない。

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());

2016年1月14日木曜日

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

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を参照。