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を使うのか分かっている場合は前提とする必要はない。
たとえばどのクラスも高速なムーブ操作が提供されていると分かっているのならば、コピー操作をムーブ操作に置き換えることでムーブセマンティクスの恩恵を受けることができる。