ラベル cpp の投稿を表示しています。 すべての投稿を表示
ラベル cpp の投稿を表示しています。 すべての投稿を表示

2017年1月17日火曜日

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

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

Item 28: reference collapsing (参照の縮約)

universal referenceでは引数にlvalueとrvalueのどちらが渡されたかの情報を持つ。

template<typename T>
void func(T&& param);

universal referenceではlavalueが渡されるとTがlvalue referenceと型推定され、rvalueが渡されるとTはnon-reference (参照型では無い)とされる。

Widget widgetFactory(); // rvalueを返す関数
Widget w;
func(w); // TはWidget&と型推定される
func(widgetFactory()); // TはWidgetと型推定される

reference collapsing (参照の縮約)

C++では参照の参照は許されない。

int x;
...
auto& & rx = x; // 参照の参照の宣言は不可 (コンパイルエラー)

template関数funcでは参照の参照が出現しているはず。
lvalue referenceを入れた場合に実体化される関数は以下のようになるからだ。

void func(Widget& && param);

しかしコンパイラはtemplateの実体化などの特別な文脈ではreference collapsing (参照の縮約) を行い、以下のように解釈される。

void func(Widget& param);

参照にはlvalue referenceとrvalue referenceの2種類あるから、参照の参照には4種類ある。

縮約のルール: どちらかのreferenceがlvalue referenceである場合はlvalue referenceになり、そうでなければrvalue referenceとなる。

上述の例の場合はlvalue reference Widget&へのrvalue referenceであるから、lvalue refereneceであるWidget&となる。

std::forward

std::forwardはこのreference collapsingが鍵となる。

template<typename T>
void f(T&& fParam)
{
  ...
  someFunc(std::forward<T>(fParam));
}

上記の典型的なstd::forwardの使用例において、fParamはuniversal referenceであるからTは引数としてlvalueとrvalueのどちらが渡されたのか(つまりfParamがどちらで初期化されたのか)を知っている。
したがってfにrvalueが渡された時、つまりTがnon-referenceと型推定される時にのみfParam (lvalue) がrvalueにキャストされる。

template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
  return static_cast<T&&>(param);
}

上記のコードは(一部のインターフェースの詳細が省かれているため)標準規格準拠では無いが、std::forwardの振る舞いを示すコードとして十分である。

fにWidget型のlvalueが渡された場合

TはWidget&と型推定され、std::forwardはstd::forward<Widget&>として実体化される。

Widget& && forward(typename remove_reference<Widget&>::type& param)
{ return static_cast<Widget& &&>(param);}

std::remove_reference<Widget&>::typeはWidgetとなる。

Widget& && forward(Widget& param)
{ return static_cast<Widget& &&>(param);}

reference collapsingによってWidget& &&はWidget&に。

Widget& forward(Widget& param)
{ return static_cast<Widget&>(param);}

結果としてキャストは何もせず、lvalue referenceを返す。

fにWidget型のrvalueが渡された場合

TはWidgetと型推定され、std::forwardはstd::forward<Widget>として実体化される。

Widget&& forward(typename remove_reference<Widget>::type& param)
{ return static_cast<Widget&&>(param);}

std::remove_referenceをnon-referenceに適用してもWidgetのままであるから以下のようになる。

Widget&& forward(Widget& param)
{ return static_cast<Widget&&>(param);}

ここではreferenceに対するreferenceは無いためreference collapsingも起こらない。
結果としてstd::forwardはrvalueを返す。
つまり前段落のstd::forwardを呼び出すコードにおいて、fParamをrvalueにキャストしてsomeFuncへと渡すことになる。

ちなみにC++14にはstd::remove_reference_tという関数でより簡潔に記述できる。

template<typename T>
T&& forward(remove_reference_t<T>& param)
{ return static_cast<T&&>(param);}

reference collapsingが起こる4つのコンテキスト

  • templateの実体化
  • auto変数
  • typedefとalias declaration
  • decltype

autoについてはtemplateの実体化と同様。

auto&& w1 = w; // => Widget& && w1 = w;
Widget& w1 = w; // と解釈される
auto&& w2 = widgetFactory();
Widget&& w2 = widgetFactory(); // と解釈される

typedefについては以下の通り。

template<typename T>
class Widget {
public:
  typedef T&& RvalueRefToT;
  ...
};
Widget w;
// => typedef int& && RvalueRefToT;
// => typedef int& RvalueRefToT;

decltypeについてはitem 3を参照。

以上のことを踏まえると、universal referenceは何ら新しいreferenceではなく、実際には以下の条件を満たすrvalue referenceであることが分かる。

  • lvalueとrvalueを区別して型推定が行われる、つまりTのlvalueはT&と型推定され、TのrvalueはTと型推定される
  • reference collapsingが起こる

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

2015年12月3日木曜日

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

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

Item 14: 例外を投げない場合はnoexceptで関数を宣言しよう

C++98における例外ととC++11における例外

コンパイラは例外の一貫性の維持を手助けしてくれないため、ある関数が投げる例外に変更が加わると呼び出し元のコードを壊す可能性がある。
開発者が関数が発生しうる例外型を記述し、変更の際は呼び出し元のコードも含めて修正を加えなければならない。
C++98において例外は苦労して使用するに値しないと考えられていた。

C++11ではnoexceptと記述することで関数が例外を出さないことを保証するようになった。
最も意味のある情報はその関数が「例外を投げるのか投げないのか」という0か1の情報だというコンセンサスが(C++11策定時に)得られたため。

noexceptをつけるかどうかはインターフェースデザインの範疇

クライアントコードにおいて関数が例外を投げるかどうかは重要な問題。
例外を投げないのにnoexceptをつけない関数は悪いインターフェースデザイン。
constと同じように例外を投げない関数には常にnoexceptを指定すべき。

コンパイラの最適化とパフォーマンスへの影響

noexceptを指定した場合は例外が発生した場合にstd::terminateでプログラムが強制終了されるようになっていて、コールスタックを管理するオーバーヘッドが削減される。
また、例外が発生して関数から出る時にオブジェクトの生成と逆順に破棄する必要もなくなり、コンパイラの最適化がかかりやすい。

RetType function(params) noexcept; // most optimizable
RetType function(params) throw(); // less optimizable
RetType function(params); // less optimizable

vectorの場合はパフォーマンスに影響する典型的な例。

std::vector<Widget> vw;
Widget w;
vw.push_back(w);

vectorはpush_backする時に容量を超えると別のメモリ領域に要素を丸々コピーしてから古いメモリ領域のオブジェクトを破棄する。
これによって強い例外保証を提供していて、コピーの途中で例外が発生してもvectorの元の状態は保存される。
C++11のムーブセマンティクスを適用する場合は元のデータを書き換えてしまうためこの強い例外保証を脅かす。
しかしmove操作がnoexceptであれば安全であり、moveを利用することでパフォーマンスが改善される。
std::vector::reserveやstd::deque::insertなど別のメモリ領域への割当が発生するメソッドについても同様のことが成り立つ。

条件付きnoexcept

noexceptには条件分岐も可能(true/falseを与えてtrueであればnoexcept)。
noexceptに関数呼び出しを与えるとnoexceptの有無がtrue/falseとして解釈されてnoexceptになるかを判定してくれる。
std::swapは以下の様な実装になっている。

template <class T, size_t N>
void swap(T (&a)[N], T (&b)[N]) noexcept(noexcept(swap(*a, *b)));

template <class T1, class T2>
struct pair {
    …
    void swap(pair& p) noexcept(noexcept(swap(p.first, p.first)) && noexcept(swap(p.second, p.second)));
    …
};

swapはSTLの様々なアルゴリズムで使用されているため、例外が発生しないならnoexceptを指定して最適化されることが重要。

noexcept指定の注意点

noexceptを指定することは大きなメリットがあるが、もしnoexceptを指定した関数に修正を加えてnoexceptを指定しないように変更した場合はクライアントコードを壊す可能性がある。
noexceptは可能なら常に指定すべきだが、今後もnoexceptであり続けると考えられる関数に限るべき。
なお、全てのメモリ解放関数とデストラクタは暗黙の内にnoexceptとなっている。デストラクタにnoexcept(false)を明示的に指定すれば例外を発生させられるようになるが通常使うべきではない。

自然な実装で例外が発生しない関数に対してnoexcept指定すべきである。
例外を発生させないがために関数内でtry catchを書くことで複雑になってしまうようでは本末転倒。
パフォーマンス上の利点もtry catch文の追加によるオーバーヘッドでかき消されてしまう。

wide contractsとnarrow contracts

wide contracts: どのような引数を与えても未定義動作にならない
narrow contracts: 前提条件が存在し、その条件に反する引数が与えられると未定義動作になる
narrow contractsである関数では引数が不正な場合に未定義動作となってデバッグが困難となる。
そのような場合には例外で引数が不正であることを呼び出し側に伝えるのが親切。
それを分かっている設計者はwide contractsにはnoexceptをつけ、narrow contractsにはnoexceptをつけないことが多い。

関数が本当にnoexceptであるかはコンパイラがチェックしてくれるわけではない

void setup(); // functions defined elsewhere
void cleanup();
void doWork() noexcept
{
    setup(); // set up work to be done
    … // do the actual work
    cleanup(); // perform cleanup actions
}

setupもcleanupもnoexceptではないのにdoWorkはnoexcept指定されているが、コンパイルは通る。
setupとcleanupはCやC++98のライブラリかもしれない。そもそもstd::strlenなどのCの関数もnoexcept指定されていない。
このような関数が多数存在するので融通を利かせるためにコンパイラはチェックしないようにしている。

2015年10月20日火曜日

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

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

Item 10: スコープ無しenumよりもスコープ付きenumを使おう

scoped enumではスコープが有効

一般的な規則では"{"内の変数名はそのスコープ内でしか使えないが、C++98のenumはそのenumがあるスコープ内全体で使えてしまう

enum Color { black, white, red };
auto white = false; // エラー (スコープ内で既に同じ名前が宣言されているため)

C++11のscoped enumなら上述のような「リーク」が起こらない。

enum class Color { black, white, red };
auto white = false; // OK
Color c = white; // エラー (このスコープからは見えない)
Color c = Color::white; // OK
auto c = Color::white; // OK

その宣言の仕方から"enum class"とも呼ばれる。

scoped enumなら意図しない暗黙の型変換を防げる

unscoped enumは数値型に暗黙の型変換がされる。

enum Color {black, white, red};
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
if (c < 14.5) { // doubleと比較
    auto factors = primeFactors(c); // intに変換
}

scoped enumは暗黙の型変換が無い。

enum class Color {black, white, red};
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
if (c < 14.5) { // エラー (Colorからdoubleに変換されない)
    auto factors = primeFactors(c); // エラー (Colorからstd::size_tに変換されない)
}

scoped enumで型変換を行うにはキャストが必要。

if (static_cast<double>(c) < 14.5) {
    auto factors = primeFactors(static_cast<std::size_t>(c));
}

scoped enumは前方宣言が可能

scoped enumなら定義を記述しなくても宣言が可能

enum Color; エラー
enum class Color; // OK

C++98のunscoped enumで前方宣言ができなくなっているのは、定義を見てコンパイラがデータ型を決定しているため。 メモリを最小化するためにコンパイラがenumの定義を見て最小の型を選べるようにしていた。

enum Color { black, white, red }; // コンパイラによってはchar型を選択
enum Status { good = 0,
    failed = 1,
    incomplete = 100,
    corrupt = 200,
    indeterminate = 0xFFFFFFFF
}; // charでは表現できないのでより大きな整数型が選ばれる

前方宣言ができないならenum Statusはヘッダーファイルに書くことになる。 この時enum Statusにaudited = 500を新たに加えるとそのヘッダーファイルをインクルードしているファイルは全てリコンパイルが必要。 しかしscoped enumは前方宣言可能なので、定義を実装ファイルに分離することで無駄なリコンパイルを防げる。

scoped enumのデフォルト型はint型で、他の型にオーバーライドも可能。

enum class Status; // int型
enum class Status: std::uint32_t;
enum class Status: std::uint8_t;

もちろん定義に対しても指定可能。

enum class Status: std::uint32_t {
    good = 0,
    failed = 1,
    incomplete = 100,
    corrupt = 200,
    audited = 500,
    indeterminate = 0xFFFFFFFF
};

C++11ではunscoped enumにもこの記法が使えて、その場合は前方宣言が可能に。

enum Color: std::uint8_t; 

unscoped enumの方が便利な場面と対処法

tupleの1番目の要素を取り出したい時。

using UserInfo = std::tuple<std::string, std::string, std::size_t>;
UserInfo uInfo;
…
auto val = std::get<1>(uInfo);

unscoped enumを使えば即値の1ではなくて定数名を与えることが可能。

enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
…
auto val = std::get<uiEmail>(uInfo);

scoped enumだとくどい書き方になってしまう。

enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
…
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

ただし以下のコンパイル時に結果を返す関数を定義すれば

template<typename E>
constexpr auto toUType(E enumerator) noexcept {
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

もう少し短く書くことが可能。

auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

scoped enumには今までに挙げた大きな利点があるため、この場面において少々文字を多く打つことになってもscoped enumの方を使うことに価値がある。

2014年10月13日月曜日

Pythonのmap関数をC++で作る試み

Pythonのmapが便利なので、同じように使えそうなmapをC++14で作ってみました。
ソースはこちら。(勉強がてらなので、雑然としてます。)
サンプル: https://github.com/muumu/lib/blob/master/sample/sample_functional.cpp
ライブラリ: https://github.com/muumu/lib/blob/master/functional.h
以下、作ったfn::mapのだいたいの使用感です。

それぞれの要素に関数やメンバ関数を適用したコンテナを作る

{
    vector<string> source = {"a?", "ab?", "abc?", "abcd?"};
    auto output = fn::map(source, fn::init);
    print(output); // {"a","ab","abc","abcd"}
}
{
    list<string> source = {"", "occupied", "", "occupied"};
    auto output = fn::map(source, &string::empty);
    print(output); // {true,false,true,false}
}
{
    array<string, 4> source = {"add", "sub", "mul", "div"};
    auto output = fn::map(source, util::upper);
    print(output); // {"ADD","SUB","MUL","DIV"}
}

違うデータ型になる関数はそのデータ型のコンテナに変換

{
    set<string> source = {"tea", "wine", "milk", "coffee"};
    auto output = fn::map(source, &string::size);
    print(output); // {3,4,6}
}
{
    multiset<string> source = {"tea", "wine", "milk", "coffee"};
    auto output = fn::map(source, &string::size);
    print(output); // {3,4,4,6}
}

キーとバリューを逆転したマップの作成

{
    map<string, int> source_map = {{"RED", 0}, {"GREEN", 1}, {"BLUE", 2}};
    auto reversed_map = fn::map(source_map, fn::swap<string, int>);
    print(reversed_map); // {{0,"RED"},{1,"GREEN"},{2,"BLUE"}}
}

vectorの出力を違うコンテナで受け取る

{
    multimap<string, int> src_map = {{"RED", 0}, {"RED", 0}, {"GREEN", 1}};
    auto func = [](const pair<string, int>& pair) {
        return pair.first + ": " + util::to_string(pair.second);};
    auto result = fn::map(src_map, func);
    print(result); // {"GREEN: 1","RED: 0","RED: 0"}
    set<string> result_set = fn::map(src_map, func);
    print(result_set); // {"GREEN: 1","RED: 0"}
}

2014年8月18日月曜日

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

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

Item 7: オブジェクトを生成する時、()と{}を区別しよう

以下の文は全てint型変数を0で初期化する

int x(0);
int y = 0;
int z{0};
int z = {0};

初期化と代入の区別

Widget w1; // デフォルトコンストラクタが呼ばれる
Widget w2 = w1; // コピーコンストラクタが呼ばれる
w1 = w2; // 代入(operator=が呼ばれる)

C++11で追加された{} (braced initialization) はuniform initializationの実装。
{}を使うと、C++11以前はできなかったコンテナの初期化が可能。

std::vector<int> v{1, 2, 3};

初期化方法に違いが生じるケース

C++11では、非staticデータメンバの初期化が可能になったが、()ではできない

class Widget {
private:
    int x{0}; // OK
    int y = 0; // OK
    int z(0); // エラー!
}

std::atomicsなどのコピー不可能なオブジェクトは=で初期化できない

std::atomic<int> ai1{0}; // OK
std::atomic<int> ai2(0); // OK
std::atomic<int> ai3 = 0; // エラー!
{}は組み込み型のnarrowing conversionを禁止する
double x, y, z;
...
int sum1{x + y + z}; // エラー! doubleからintへの変換に非対応
int sum2(x + y + z); // OK double型の値がint型に丸められる
int sum3 = x + y + z; // OK 同上
{}はmost vexing parseを起こさない
Widget w1(10); // 10を引数に取るWidgetのコンストラクタが呼ばれる
Widget w2(); // most vexing parse! Widget型を返す関数の宣言と解釈される
void f(const Widget& w = Widget()); // Widgetのデフォルトコンストラクタが呼ばれる

Widget w1{10}; //コンストラクタが呼ばれる
Widget w2{}; // デフォルトコンストラクタが呼ばれる
void f(const Widget& w = Widget{}); //デフォルトコンストラクタが呼ばれる

{}の直感に反する動作

autoは、{}で初期化されるオブジェクトをstd::initializer_list型と型推定する

auto v1 = -1; // v1はint型
auto v2(-1); // v2はint型
auto v3{-1}; // v3はstd::initializer_list<int>型
auto v4 = {-1}; // v4はstd::initializer_list<int>型

std::initializer_listの無いコンストラクタは通常の動作

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    ...
};

Widget w1(10, true); // 1番目のコンストラクタが呼ばれる
Widget w2{10, true}; // 同上
Widget w3(10, 5.0); // 2番目のコンストラクタが呼ばれる
Widget w4{10, 5.0}; // 同上

しかしstd::initializer_listがある場合、{}は常にそちらを優先する

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<long double> il);
    ...
};

Widget w1(10, true); // 1番目のコンストラクタが呼ばれる
Widget w2{10, true}; // std::init_listのコンストラクタが呼ばれる
                     // 10とtrueはlong doubleに変換される
Widget w3(10, 5.0); // 2番目のコンストラクタが呼ばれる
Widget w4{10, 5.0}; // std::init_listのコンストラクタが呼ばれる
                     // 10と5.0はlong doubleに変換される

narrowing conversionが生じてコンパイルエラーになってもstd::initializer_listを優先する

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<bool> il);
    ...
};

Widget w{10, 5.0}; // narrowing conversionによるコンパイルエラー!

しかし変換が存在しない場合は他の候補を読みに行く

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<std::string> il);
    ...
};

Widget w1(10, true); // 1番目のコンストラクタが呼ばれる
Widget w2{10, true}; // 1番目のコンストラクタが呼ばれる
Widget w3(10, 5.0); // 2番目のコンストラクタが呼ばれる
Widget w4{10, 5.0}; // 2番目のコンストラクタが呼ばれる

空の{}は空のstd::initializer_listではなく引数が無いことを表す

class Widget {
public:
    Widget();
    Widget(std::initializer_list<int> il);
    ...
};

Widget w1; // デフォルトコンストラクタが呼ばれる
Widget w2{}; // デフォルトコンストラクタが呼ばれる
Widget w3(); // most vexing parse! 関数が宣言される
Widget w4({}); // std::init_listのコンストラクタが呼ばれる(リストは空)
Widget w5{{}}; // 同上

コピー・ムーブコンストラクタは通常通り呼ばれる
(Widget型であれば、たとえint型への変換が定義されていても、コピーコンストラクタ・ムーブコンストラクタが呼ばれる)

class Widget {
public:
    Widget(const Widget& rhs);
    Widget(Widget&& rhs);
    Widget(std::initializer_list<int> il);
    operator int() const;
    ...
};

auto w6{w5}; // コピーコンストラクタが呼ばれる
auto w7{std::move(w5)}; // ムーブコンストラクタが呼ばれる

これらのような難解なルールは、std::vectorという身近な例にも影響を与える。

std::vector<int> v1(10, 20); // 20を値とする要素を10個持つvector
std::vector<int> v2{10, 20}; // 10と20の2つの要素を持つvector

クラスデザインのガイドライン

ユーザーが()を使っても{}を使っても同じコンストラクタが呼ばれるデザインが良い。(その点においてstd::vectorは悪い例。)

std::initializer_listを引数に取るコンストラクターが無い状態からそれを新たに加える場合、既存のコードがその新しいコンストラクターを呼び出すように変化する危険性が生じる。
そのような改変は、十分に熟考してから行うべきである。

()と{}の利用ガイドライン

選択肢1: 原則{}を使う

{}は最も広範囲に適用でき、narrowing conversionを防ぎ、C++のmost vexing parseを起こさないという理由で{}を使用する。 ただし、先述のように{}を使用できないいくつかの場合において()を使用する。

選択肢2: 原則()を使う

()はC++98の文法と互換性があり、autoによるstd::initializer_list型推定の問題を回避し、std::initializer_listコンストラクタが意図せず呼び出されることが無いということを重んじて、()を使用する。 ただし、vectorの初期化などのように{}が必要な場面では{}を使用する。

片方の選択肢がもう片方より優れているということはない。
どちらかを選び、一貫した姿勢でコードを書くと良いだろう。

template関数やクラスを設計する場合はさらに事情が複雑になる。

template<typename T, typename... Args>
void doSomeWork(const T& obj, Args&&.. args)
{
    T localObject(std::forward<Args>(args)...);
    T localObject{std::forward<Args>(args)...};
}

これにたとえばユーザーがvectorを入れると、先述のように異なる結果になってしまう。

std::vector<int> v;
...
doSomeWork(v, 10, 20);

std::make_uniqueとstd::make_sharedはこの問題に直面し、()を採用してその旨をインターフェースに記述することで解決した。 他には、タグディスパッチによる解決方法もある。(Item 29)

Things to Remember

  • {}による初期化は最も広く適用可能な初期化であり、narrowing conversionを防ぎ、most vexing parseを起こさない
  • Item 2の通り、autoは{}で初期化されたオブジェクトをstd::initializer_list型と型推定する
  • {}による初期化は、std::initializer_listをパラメータに持つコンストラクタがある場合、たとえ他のコンストラクタの方がマッチしててもそちらが呼ばれる。
  • ()と{}のどちらで初期化するかで違いが生じる例はstd::vectorの2引数コンストラクタ
  • template内のオブジェクト生成において()と{}のどちらを選ぶかという決断は困難になりうる

2014年8月9日土曜日

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

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

Item 6はproxy classに対して使用するtyped initializerイディオムが紹介されていました。 どちらも初見の用語でした。 このイディオムを使用するのは一見無駄に見えましたが、確かにコードに意図を込めるには必要なのかなと思いました。

Item 6: typed initializerイディオムに気をつけよう

autoが意図しない型推定をしてしまうケース

std::vector<bool> features(const Widget& w);

// OK
auto highPriority = features(w)[5];
processWidget(w, highPriority);

// 未定義動作に!
auto highPriority = features(w)[5];
processWidget(w, highPriority);

autoにするとhighPriorityはboolではなくstd::vector<bool>::reference型になる。 bool&型にならない理由は、std::vector<bool>はビットで真偽値を格納するように特殊化されており、bool&のように振る舞う型を返すため。 features(w)はstd::vector<bool>の一時オブジェクトを返すことに注意。 この一時オブジェクトはhighPriorityの初期化の命令文が終わった時点で破棄され、std::vector<bool>::referenceは無効なポインタとなってしまう。

proxy class

std::vector<bool>::referenceは、他の型をエミュレートしたり増強したりする"proxy class"の例。 スマートポインタもproxy class。 proxy classの有用性はデザインパターンの"Proxy"パターンに由来する。 スマートポインタはstd::shared_ptrのように目に見えるproxy、std::vector<bool>::referenceは目に見えないproxy。

以下はExpression templatesもproxy classを使用した例。

Matrix sum = m1 + m2 + m3 + m4;

operator+がMatrixオブジェクトではなくSum<Matrix, Matrix>というproxy classを返すことで無駄な処理を省いて高速化する。 結果として右辺はSum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>型となり、それがMatrix型に暗黙の型変換がされる。

一般的に、目に見えないproxy classはautoと相性が悪い。 proxy classのオブジェクトはたいてい一時オブジェクトで、文が終わる時に寿命も尽きてしまうためだ。

目に見えないproxy classが使われているかどうかは、そのライブラリのドキュメントやヘッダーファイルを読めば分かる。

template <class Allocator>
class vector<bool, Allocator> {
    public:
    class reference{ ... };

    reference operator[](size_type n);
    ...
};

typed initializerイディオム

proxy classに対してはtyped initializerイディオムを使うと良い。

auto highPriority = static_cast<bool>(features(w)[5]);
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

このメリットは以下の例のように、戻り値と異なる型を指定しているのが意図的であることをはっきり示す点である。

double calcEpsilon();
float ep = calcEpsilon(); // 誤ってfloatを指定したのかと疑われる
auto ep = static_cast<float>(calcEpsilon()); //意図的にfloatにしたのだと分かる

// dは0.0と1.0の間の小数だとする
int index = d * c.size(); // doubleからintへの変換の意図が弱い
auto index = static_cast<int>(d * c.size()); // こちらの方が意図が伝わる

Things to Remember

  • 目に見えないproxy classはautoに「誤った」型推定をさせてしまう
  • typed initializerイディオムでautoに意図した型を推定させることができる

2014年8月2日土曜日

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

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

第2章はautoについてです。
item 5では、変数宣言にautoを使うとどのようなメリットがあるのかが書かれていました。

Chapter 2 auto

autoのアイデア自体はC++の作者であるBjarne Stroustrupが持っていたが、当時としては冒険的すぎたので眠っていた。
それが30年近く経って、C++11に登場した。
おそらくC++11で最も使用される機能だが、時に直感に反する挙動をするので正しく使用するように注意する必要がある。

Item 5: 目地的な型宣言よりもautoを使おう

メリット1. 変数の初期化忘れを防げる

int x; // 変数が初期化されていない
auto x; // イニシャライザが無いので型推定できずコンパイルエラー
auto x = 0; // OK。xは正しく定義されている

メリット2. ローカル変数の宣言が簡潔に書けるように

// autoを使わない場合
template<typename IT>
void dwim(It b, It e)
{
    while (b != 0) {
        typename std::iterator_traits<It>::value_type
            currValue = *b;
        ...
    }
}

// autoを使った場合
template<typename IT>
void dwim(It b, It e)
{
    while (b != 0) {
        auto currValue = *b;
        ...
    }
}

メリット3. ラムダ式を格納する最も効率的な型として振る舞う

ラムダ式はコンパイラしか知らない型だが、autoを使えばそれを表現できる。

auto derefUPLess =
    [](const std::unique_ptr<Widget>& p1,
       const std::unique_ptr<Widget>& p2)
    { return *p1 < *p2; }

C++14では、パラメータもautoにすることが可能。

auto derefUPLess =
    [](auto& p1,
       auto& p2)
    { return *p1 < *p2; }

std::functionを使って下記のように記述することも可能だが、autoに比べて記述が長くなる上、std::functionには実行速度や使用メモリのオーバーヘッドがある。
また、ヒープを使用する場合もあるので、out-of-memory例外が発生する可能性もある。
autoはstd::bindの結果を格納するのにも有効である。(ただしitem 36にあるように、std::bindよりもラムダ式の方が好ましい。)

std::function<bool(const std::unique_ptr<Widget>&,
                   const std::unique_ptr<Widget>&)>
    derefUPLess = [](const std::unique_ptr<Widget>& p1,
                     const std::unique_ptr<Widget>& p2)
                    { return *p1 < *p2; }

メリット4. 小さい型への代入を防げる

std::vector<int> v;
unsigned sz = v.size();
auto sz = v.size(); // std::vector<int>::size_typeとなる

32bit環境では問題ないが、64bit環境では(32bitである)unsigned型だと問題が起こる可能性がある。
64bit環境ではstd::vector<int>::size_typeは64bit整数であり、unsigned型に入れると32bit整数の最大値を超える整数はオーバーフローにより不正な値になってしまう。

メリット5. 明示的な型指定で起こりやすいミスを防げる

std::unordered_map<std::string, int> m;
...
for (const std::pair<std::string, int>& p : m)
{
    ... // pで何かをする
}

std::unordered_mapのkeyはconstであるという仕様のため、std::pair<const std::string, int>が正しい。
上記の例のようにconst std::pair<std::string, int>&としてしまうと、コンパイラはstd::pair<const std::string, int>からstd::pair<std::string, int>の一時オブジェクトを生成し、それをconst参照にバインドする、という無駄な処理を行ってしまう。
以下のように記述すればこのミスを防げる。

for (const auto& p : m)
{
    ...
}

メリット4とメリット5はどちらも暗黙の型変換ため気づきにくいミスをautoが防いでくれるという例。

以上のようにautoを使用するメリットは複数あるが、いつでもautoさえ使っておけば安心というわけではない。
その例は、Item 2 とItem 6で記述されている。

中にはautoが変数の型を分かりにくくすると主張する人がいる。
しかし、IDEを使う場合は簡単に型を表示できるし、だいたいの型でよければ、変数名を賢く設定すれば分かるものだ。
また、ソフトウェア開発コミュニティではC++以外の型推定を持つ言語や動的型付け言語の実績が数多くあり、商業規模のコードの信頼性や保守性にも十分耐えられると言える。

メリット6. リファクタリングが容易になる

autoを使用している場合、初期化時の型を変更すればautoの部分は自動的に変更が伝播するので、1つの型を変更することで何箇所も修正するという手間が無くなる。

Things to Remember

  • autoは初期化忘れを防ぎ、型指定のミスによる移植性や効率の問題を解消し、リファクタリングを用意し、タイプ数を削減する。
  • ただし、autoの落とし穴には注意する必要がある(Item 2と Item 6で記述)

2014年7月26日土曜日

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

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

Item 4では推定された型を表示するテクニックが書かれていました。 コンパイルエラーメッセージを利用したアイデアは単純で面白いです。 実行時は、プラットフォーム依存であっても__PRETTY_FUNCTION__ or __FINCSIG__を使うやり方が楽だと思いました。

Item 4: 推定された型を見る方法を知ろう

デバッグや実験、単純な疑問、それらのために型推定された型を知りたいことがある。

IDEを使う

auto x = theAnser;
auto y = &theAnser;

autoのところにカーソルを持っていけば表示されるというやつ。IDEのいいところ。

コンパイラの出力

コンパイルエラーで型の情報を見る方法。 定義の書かれていないこんなクラスを宣言し、実体化させる。

template<typename TD>
class TD; // TDは"Type Displayer"の略

TD<decltype(x)> xType;
TD<decltype(y)> yType;

するとTDは定義が無いのでコンパイルエラーになり、その時xとyの型がエラーメッセージの中に表示される。 TD<int>、TD<const int>を実体化できないという文脈で表示される。

実行時の出力

まず思いつくのはtypidからstd::type_info::nameを取り出す方法。(しかし後述の通りこの方法は誤り)

std::cout << typeid(x).name() << '\n';
std::cout << typeid(y).name() << '\n';

しかし読みやすいものが返されることは保証されていない。
GNUとClangはxの型をi、yの型をPKiと表示する。
iはint、Pはポインタ、Kはconstを意味する。
Microsoftのコンパイラはxの型をint, yの型をint const *と表示する。

template<typename T>
void f(const T& param)
{
    using std::cout;
    cout << "T = " << typeid(T).name() << '\n';
    cout << "param = " << typeid(param).name() << '\n';
}

const auto vw = createVec(); // std::vector<Widget>を返すファクトリ関数
if (!vw.empty()) {
    f(&vw[0]); // &vw[0]の型はconst Widget *
}

// 出力結果 (gcc)
// T = PK6Widget
// param = PK6Widget
// ※6はクラス名の文字数を示す

// 出力結果 (MSVC)
// T = class Widget const *
// param = class Widget const *
// "Widget const *"は"const Widget *"と同じ

Tは合っているが、paramは本来const Widget * const &のはずである。
これは、std::type_info::nameが型を値渡しされた時のように扱うため。
参照とconstが取り除かれるため、const Widget *になってしまう。

また、この場合、残念なことにIDEも解読しづらい結果を返してくる。

// Tの型
const
std::_Simple_types<std::_Wrap_alloc<std::_Vec_base_types<Widget,
std::allocator<Widget> >::_Alloc>::value_type>::value_type *

// paramの型
const std::_Simple_types<...>::value_type *const &

typedefを追っていけば実際に指している型が分かるが、長い上にparamの方は...と省略されてしまっている。

一方、コンパイラのエラーメッセージは信頼できる。

TD<T> TType;
TD<decltype(param)> paramType;

とすればコンパイル時に正しい型を表示してくれる。

実行時に正しい型情報を表示するため、std::is_constやstd::is_pointer, std::is_lvalue_referenceなどとtypeidを組合せて自作の表示関数を作るのも手だ。
しかし、プラットフォーム依存であることを許せば簡単な方法がある。

template<typename T>
void f(const T& param)
{
    #if defined(__GNUC__)
        std::cout << __PRETTY_FUNCTION__ << '\n'; // GNUとClang用
    #elif defined(_MSC_VER)
        std::cout << __FUNCSIG__ << '\n'; // Microsoft用
    #endif
}
// 出力結果
// GNU
// void f(const T&) [with T = const Widget*]
// Clang
// void f(const Widget *const &)
// Microsoft
// void __cdecl f<const classWidget*>(const class Widget *const &)

Things to Remember

  • 推定された型はIDEのエディタ上か、コンパイルのエラーメッセージか、typeidか、__PRETTY_FUNCTION__や__FUNCSIG__などの言語拡張で見ることができる。
  • しかしそれらの表示は有用でなかったり正確で無かったりするので、C++標準規格の型推定ルールを理解していることは相変わらず重要。

2014年7月24日木曜日

【読書ノート】Effective Modern C++ - Item 3 [2 / 2]

Item 3の続きです。
以下のC++14の関数にまだ改善の余地があるというお話から。

universal referenceでrvalueをサポートする

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);

この関数にはrvalueは渡せない。(Container&がconst参照でないため)
rvalueを渡すのはマイナーなケースだが、意味のある例は存在する

std::deque<std::string> makeStringDeque();
// dequeの5番目の要素のコピーを得る文
auto s = authAndAccess(makeStringDeque(), 5);

これをサポートするにはuniversal reference(詳細はItem 26)を使用すれば良い。

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i);

なお、Indexは標準ライブラリの例(std::string, std::vector, std::dequeなど)が合理的だと思えるため値渡しにしている。
加えてItem27の勧告に従ってuniversal referenceにstd::forwardを適用する。

template<typename Container, typename Index> // C++14の最終版
decltype(auto)
authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

template<typename Container, typename Index> // C++11の最終版
auto
authAndAccess(Container& c, Index i)
-> decltype(std::forward<Container>(c)[i])
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

decltypeの僅かな特殊なケース

decltypeは、変数名に関してはその変数の型を返すが、変数名以上に複雑なlvalueは全てlvalue referenceとして返す。
つまり、変数名以上に複雑な式のT型はT&型と型推定する。
しかしそのようなケースは少ない。(たとえば関数が返すlvalueはlvalue referenceである。さもなくばrvalue)
変数名以上に複雑なlvalueの例は、(x)だ。

int x = 0;
// decltype(x)はint
// decltype((x))はint&

decltype(auto) f1()
{
    int x = 0;
    ...
    return x; // f1の戻り値はint
}

decltype(auto) f2()
{
    int x = 0;
    ...
    return (x); // f2の戻り値はint&
}

f2の戻り値はローカル変数の参照なので、戻り値を受け取ったら未定義動作となってしまう!

注意点はあるが、通常の場合はdecltype(auto)は直感的な動作をする。
特に変数名に適用する場合は、decltypeという名前通り、変数の宣言された型(declared type)を返す。

Things to Remember

  • decltypeはほぼ常に変数や式の型を修正無しに生成する
  • 変数名より複雑なlvalue(たとえば"(x)")のTに対してdecltypeは常にT&を返す
  • C++14はdecltype(auto)をサポートし、decltypeのルールに従って型推定する

2014年7月23日水曜日

C++で数値を文字列に変換する最も高速な方法はBoost.Spirit.Karma

C++で数値を文字列に変換する方法は、sprintf, iostreamのstd::stringstream, boost::lexical_cast, boost::formatなどが有名ですが、最も実行速度の速い方法は他にあり、Boost.Spirit.Karmaを使用する方法です。
Boost.Spirit.Karmaの初歩的な使用方法は以下のソースコードを参考にしてください。
https://gist.github.com/muumu/d8e399c544076301e94d
また、boost公式のパフォーマンス計測が自分にとって不十分だったので、下記の改変したソースコードでパフォーマンス計測を実行しました。
https://gist.github.com/muumu/3b1801456481b447f3a2
ltoaは自分の環境に無いためコメントアウト、代わりにsnprintfを追加。(snprintfは、sprintfより安全で、同程度か僅かに速い関数です。)

パフォーマンス計測結果

前回、小数のフォーマット出力が最も高速なのがBoost.Spirit.Karmaであることを紹介したので、今回は整数値の計測を示します。
処理内容は、ランダム値を表示するものです。
(実行環境: boost: 1.54.0, gcc: 4.8.2 コンパイルオプション: -O3, -std=c++1y, CPU: Core i3 4130T)

Converting 10000000 randomly generated int values to strings.
snprintf (std::string):                 1.06302 [s]
snprintf (char[]):                      0.834953 [s]
iostreams (std::string):                1.23543 [s]
iostreams (std::stringstream):          0.673501 [s]
Boost.Format (std::string):             4.26696 [s]
boost::lexical_cast (std::string):      0.806607 [s]
Karma int_ (std::string):               0.326372 [s]
Karma int_ (char[]):                    0.131378 [s]
Karma (std::string):                    0.254512 [s] // 直接std::stringに出力するkarma
// ()内は出力形式。
// char配列型で()内がstd::stringなものは、出力結果を改めてstd::string型に代入してます

Karmaはsnprintfの実に5倍前後高速な結果となりました。 Boost.Spirit.Karmaが何故こんなに高速なのか、興味が出たので今後調査したいと思っています。

2014年7月20日日曜日

Boost.Spirit.Karmaのフォーマット出力が異常に早い

Comparing the performance of a sequence of several generatorsを見てBoost.Spirit.Karmaのフォーマット出力が優秀だと知ったのですが、最近のgccとboostだとどうなるかと思って自分のLinux環境でも実行してみました。
(実行環境: boost: 1.54.0, gcc: 4.8.2 コンパイルオプション: -O3, -std=c++1y, CPU: Core i3 4130T)
ソースは上記ページのものにsnprintfも加えたものを実行しました。(最近はセキュルティの観点でsprintfよりsnprintfが推奨されるため)
処理内容は、12345.12345という小数2つから[12345.123 12345.123 ]という文字列(小数点以下3桁、14文字分のスペース)を繰り返し生成するものです。
すると、Spirit.Karmaがsprintfの4倍近く速いことが分かりました。(-O2オプションでもほとんど同じ結果でした。)

sprintf:        1.13721
snprintf:       1.12995
iostreams:      1.39023
format:         1.99459
karma:          0.231521
karma (string): 0.318302
karma (rule):   0.294326

※iostremasはstd::stringstream、formatはboost::formatを指しています。

また、元々のソースでは12345.12345が直に書かれていたので、定数の最適化が行われている可能性を考えて標準入力から数値を与えるようにソースを改変してみましたが、結果はほとんど変わりませんでした。
疑いようもなくSpirit.Karmaが速いです。

input a decimal
12345.12345
input one more decimal
12345.12345
sprintf with args:      1.14024
snprintf with args:     1.12748
iostreams with args:    1.38619
format with args:               2.01176
karma with args:                0.228151
karma (string) with args:       0.314551
karma (rule) with args: 0.293565

なお、数値の桁数が小さい時は差が縮まりました。

input a decimal
1.0
input one more decimal
2.0
sprintf with args:      0.412689
snprintf with args:     0.393523
iostreams with args:    0.640817
format with args:               1.2292
karma with args:                0.263695
karma (string) with args:       0.327404
karma (rule) with args: 0.307239

Boost.Spirit.Karmaはコードが複雑でコンパイル時間も増えるのでカジュアルに使うのには向かないですが、処理速度を求める時には最適なようです。
カジュアルに使う場合は、処理速度は遅いですがsprintf風の記法で型安全でもあるboost::formatが使いやすいですね。
std::stringstreamは、数値を入れる時に余計なバッファを確保するので遅いようです。(boost::formatよりは速いですが。)

ちなみに、int型をstd::string型に変換するのみの場合でもSpirit.Karmaは速いようです。
C++ Convert Int to String Speed
これを見てもうひとつ分かるのは、boost::lexical_castがsprintf以上に速いということです。
カジュアルに数値を文字列に変換するだけなら、boost::lexical_castがベストチョイスになりそうです。
(C++11以降ではstd::to_stringという選択肢もありますが、こちらの処理速度等の詳細は未確認です。)

【読書ノート】Effective Modern C++ - Item 3 [1 / 2]

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

Item 3はdecltypeについてです。
自分はC++11の関数の戻り値を後ろに書く記法とC++14のdecltype(auto)をちゃんと知らなかったので、学ぶところが多かったです。

Item 3: decltypeを理解しよう

decltypeは名前や式の型を教えてくれる。
典型的の場合は予想通りの動きをしてくれるが、時には予想に反する動きをする。

典型的な場合

const int i = 0; // decltype(i)はconst int
bool f(const Widget& w); // decltype(w)はconst Widget&
    // decltype(f)はbool(const Widget&)

struct Point {
    int x, y; // decltype(Point::x)はint。Point::yも同様。
};

Widget w; // decltype(w)はWidget

if (f(w)) ... // decltype(f(w))はbool

template<typename T>
class vector {
public:
    ...
    T& operator[](std::size_t index);
    ...
};

vector v; // decltype(v)はvector
...
if (v[0] == 0) ... // decltype(v[i])はint&

関数の戻り値の推定

C++11の場合

主なdecltypeの使用場所はtemplate関数で、テンプレートパラメータに依存する型を返したい時だ。

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) // 改善の余地あり(後述)
    -> decltype(c[i])
{
    authenticateUser();
    return c[i];
}

これにより、c[i]が何の型を返そうともそれをそのまま関数の戻り値とすることができる。
なお、この関数名の前のautoは型推定とは関係ない。
このautoは単にC++11のtrailing return type syntax(戻り値を後置する記法)が使用されることを示す。
この後置のメリットは、戻り値の型の計算に関数パラメータを使用できること。
(上記例ではcとi。後置を使わないと、cとiは利用できない。)

C++14の場合

C++14では、以下のようにautoだけで戻り値の型推定が行われるようになった。
(この場合はもちろん、autoは型推定を示すキーワードとなる。)

template<typename Container, typename Index> // C++14の記法 + 改善の余地あり
auto authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return c[i]; // 戻り値の型がc[i]から推定される
}

しかしこの場合、型推定のルールは?autoとdecltypeのどちらかのもの?
驚くかもしれないが、答えはテンプレートの型推定のルールになる。
ただ、初期化リスト{}を除けばautoとテンプレートの型推定のルールは同じだ。
しかし今回の場合はこのルールだと問題になる。
operator[]は大抵参照を返すが、テンプレートの型推定では参照を取り除いてしまうからだ。
従って、以下のコードはコンパイルエラーになる。

std::deque d;
...
authAndAccess(d, 5) = 10; // コンパイルエラー

これは、d[5]はint&を返すがテンプレートの型推定によって戻り値の型はintになるため。
すると、rvalueに10を代入しようとしていることになるため、コンパイルエラーとなる。
この問題は、decltype(auto)指定子によってdecltypeの型推定をさせるようにすれば解決する。

template<typename Container, typename Index> // C++14の記法 + 改善の余地あり
decltype(auto)
authAndAccess(Container& c, Index i)
{
    authenticateUser();
    return c[i];
}

変数宣言にdecltype

decltype(auto)は変数宣言にも有効な場面がある。

Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // myWidget1の型はWidget
decltype(auto) myWidget2 = cw; // myWidget2の型はconst Widget&

authAndAccessで改善の余地があるとされていることと、decltypeの型推定の典型的でない場合については、次回の記事で。

2014年7月17日木曜日

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

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

Item 2は、Item 1を理解していればすぐ読み終わる内容でした。
ただ、auto x = {27};でxがinitializer_list型になるのは知らなかったので新鮮でした。

Item 2: autoの型推論を理解しよう

autoの型推論は、1つの例外をのぞいてテンプレートの型推定と同じ。

autoをテンプレート関数のパラメータの型だと思えば同じだと分かる。

auto x = 27; // case 3 (xはポインタでも参照でもない)、xはint
const auto cx = x; // 同上、cxはconst int
const auto& rx = x; // case 1 (rxはuniversalでないreference)、rxはconst int&
auto&& uref1 = x; // xはintでlvalueなので、uref1の型はint&
auto&& uref2 = cx; // cxはconst intでlvalueなので、uref2の型はconst int&
auto&& uref3 = 27; // 27はintでrvalueなので、uref3の型はint&&

const char name[] = "R. N. Briggs"; // nameの型はconst char[13]
auto arr1 = name; // arr1の型はconst char*
auto& arr2 = name; // arr2の型はconst char(&)[13]

void someFunc(int, double); // someFuncは関数で、型はvoid(int, double)
auto func1 = someFunc; // func1の型はvoid (*)(int, double)
auto func2 = someFunc; // func2の型はvoid (&)(int, double)

ここまではItem 1と本質的に同じ。

autoとテンプレートの型推定のただ1つの違い

以下の4つは皆int型変数を27で初期化する文。
(下の2つはC++11で導入されたuniform initialization)

int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};

しかし、autoを使うと同じでなくなる。

auto x1 = 27; // int型で、値は27
auto x2(27); // 同上
auto x3 = {27}; // std::initializer_list<int>型で、値は{27}
auto x4{27}; // 同上

以下の文はstd::initializer_list<T>のTを推定できないためコンパイルエラーになる。

auto x5 = {1, 2, 3.0};

まずx5は{}の存在によりstd::initializer_listと推定される。
次に、std::initializer_listはテンプレートなのでstd::initializer_list<T>のTが決まらなければならない。
しかし、{}内の数値型が同じではないのでTが推定できずコンパイルエラーとなる。
この{}による初期化のみがテンプレートとautoの型推定の違い。

なぜこのような仕様になっているか、説得力のあるな説明はない。ルールである。
この落とし穴のため、{}による初期化はそうしなければならない時のみに使用する開発者もいる。
({}を使わなければならない場面はItem 7に説明がある。)

C++11の範囲では話は以上だが、C++14では続きがある。
C++14ではラムダ式でautoが使える。
そしてこのautoはテンプレートの型推定を行う。({}をstd::initializer_listとは推定しない。)
従って、下記のコードは{ 1, 2, 3 }の型を推定できないのでコンパイルに失敗する。

auto createInitList()
{ return { 1, 2, 3 }; // コンパイルエラー } std::vector v; ... auto resetV = [&v](const auto& newValue) { v = newValue; } ... resetV( { 1, 2, 3 } ); // コンパイルエラー

Things to Remember

  • autoの型推定は通常テンプレートの型推定と同じ
  • 唯一の例外はautoと{}による初期化を使用した変数宣言で、そこでautoはstd::initializer_list型を推定する
  • テンプレートの型推定は{}による初期化リストを与えられると失敗する

【読書ノート】Effective Modern C++ - Item 1 [2 / 2]

今回はItem 1の残り、配列型と関数ポインタ型の型推定です。
配列型は、自分でも関数に渡すとポインタ型に退化してしまって要素数が取れなくなるのを不便に感じたことがあるのですが、テンプレートで参照渡しにするとその問題が解決するのを不思議に思っていましたが、今回の説明を読んでそれが仕様通りの現象であることを理解できました。

配列引数

配列型とポインタ型は同一視されがちだが、実は違う
配列のポインタ型は、配列型が配列の先頭要素へのポインタに壊変したもの

const char name[] = "J. P. Briggs"; // nameの型はconst char[13]
const char * ptrToName = name; 配列型はポインタ型に壊変した

しかし、パラメータ型は強制的にポインタ型に壊変してしまう
void myFunc(int param[]);
これは以下の宣言と同じになってしまう
void myFunc(int* param);

これはC++がC言語を元としているため生じたもの。
下記のように値渡しのテンプレート関数では、配列を渡すとポインタ型になる。
template <typename T>
void f(T param); // 値渡しのテンプレート関数

f(name); // nameはconst char[13]型だが、const char *型と型推定される
しかし、参照渡しのテンプレート関数では、配列の参照型にできる

template <typename T>
void f(T& param); // paramはconst char (&)[13]型となる

f(name); // nameはconst char [13]型
これを使うと、コンパイル時定数として配列の要素数を返すconstexpr関数を定義できる
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) {
    return N;
}

int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; 

int mappedVals[arraySize(keyVals)]; 

関数引数

配列型と同じように、関数型も関数ポインタ型に壊変する

void someFunc(int, double); // 型はvoid(int, double)

template<typename T>
void f1(T param); // 値渡し

template<typename T>
void f2(T& param); // 参照渡し

f1(someFunc); // 値渡しは関数ポインタ型void (*)(int, double)に。

f2(someFunc); // 参照渡しは関数の参照型void (&)(int, double)に。

Things to Remember

  • ポインタ型とUniversa Referenceでない型のパラメータでは、引数が参照型かどうかは無視される
  • パラメータがUniversal Referenceの場合は、lvalueはlvalue referenceに、rvalueはrvalue referenceになる
  • パラメータが値渡しの場合、引数が参照型かどうか、constかどうかは無視される
  • 配列型と関数型の引数は、パラメータが参照型でない限り、ポインタ型に壊変する