LoginSignup
237
248

More than 5 years have passed since last update.

Effective Modern C++ メモ

Last updated at Posted at 2015-05-12

自分にとっての要点メモ。

Effective C++の著者のScott MeyersによるC++11/14の定番本(らしい)
※知らずに買ったら英語だったので、なんとか通読しましたが理解は随所怪しいと思われます。誤りは是非ご指摘ください。

2015/11/02追記: 日本語版が出ています。本稿は日本語訳が出る前に書いたのなので目次の訳は一致しません。ご注意ください。 https://www.oreilly.co.jp/books/9784873117362/

第1章: 型推論

  • autodecltypeによる型推論はコードの冗長性を減らす強力な機構だが、型推論の動きをしっかり理解していないとmodern C++での効率的なプログラミングはほぼ不可能。

1: テンプレート型推論を理解しろ

  • 関数テンプレートtemplate<typename T> void f(ParamType param);を基本に考え、f(expr)というコードを書いたときにTが何と推論されるかを論じる。

型推論のシナリオは3種類(+例外1種)。

  • ケース1: ParamTypeは参照またはポインタ型だがuniversal refernce(T&&)ではない
    • exprから参照を外してからParamTypeと照合してTを決める
  • ケース2: ParamTypeが universal reference(T&&)
    • exprがlvalueならTParamTypeはlvalue参照。これは2つの意味でトリッキー。
      1. これはTが参照型と推論される唯一のケース。
      2. ParamTypeは文法的にはrvalue referenceとして宣言されているのに型推論の結果はlvalue referenceになる。
    • exprがrvalueならケース1どおりの動作。
  • ケース3: ParamTypeがポインタでも参照でもない
    • exprから参照とCV修飾子を外す(値渡しなのでCV修飾子もなくなる)。ただしポインタや参照が指す先のオブジェクトのconst性は保存される。
  • (例外:配列・関数)
    • 普段は配列は先頭ポインタに自動変換されるが、ParamTypeが参照修飾子を含むときだけparamは「配列への参照」型となる。関数ポインタについても同様だがコーナーケースなので気にしなくていい。

int x = 27;
const int cx = x;
const int& rx = x;

template<typename T> void f1(T& param); // ケース1
f1(x);  // T is int, param is int&
f1(cx); // T is const int, param is const int&
f1(rx); // T is const int, param is const int& 

template<typename T> void f2(T&& param); // ケース2
f2(x);  // x is lvalue: T is int&, param is int&
f2(cx); // cx is lvalue: T is const int&, param is const int&
f2(rx); // rx is lvalue: T is const int&, param is const int&
f2(27); // 27 is rvalue: T is int, param is int&& 

template<typename T> void f3(T param); // ケース3
f3(x);  // T and param are both int
f3(cx); // T and param are both int
f3(rx); // T and param are both int

2: auto型推論を理解しろ

  • autoと関数テンプレートは基本同じだが、1つだけ例外がある。auto x={27};またはauto x{27};と宣言したときだけ、xはint型でなくstd::initializer_list<int>型となる。
  • C++14での関数戻り値型指定やラムダ式でのautoは上記のauto型推論ではなく#1のテンプレート型推論に従う。

3: decltypeを理解しろ

  • 名前(変数名とか関数名とか)に適用した場合のdecltype(x)は直感どおり動くので心配ない
    • decltypeはautoと違って参照型を外したりしない。
  • 名前をそのまま書く形以外の、T型に相当するlvalue expressionに対してはdecltypeはいつもT&型を報告する。()をつけるだけで「名前をそのまま書く形でない」とみなされる。例えば、xがintのときdecltype((x))はint&なので注意!
  • C++14ではdecltype(auto)が使える。"ふつうのautoでなくdecltypeのルールで型推論してね"の意。

4: 推論された型を表示する方法を知っておけ

  • IDEの機能を使う: 嘘をつかれることがある。
  • コンパイラにエラーを吐かせて観察する。template<typename T> class TD;という定義のないクラステンプレートを宣言しておく。xの型が知りたければTD<decltype x> xType;としてインスタンス化を試みると"TCがインスタンス化できないよ"的なエラーメッセージをコンパイラが吐いてくれるはず。
  • ランタイムでのprintfアプローチ: std::type_info::nameは信用できない。仕様上参照とCV修飾子は無視されることになっている。boost::typeindexライブラリを使うのがよい。
#include <iostream>
#include <boost/type_index.hpp>

template<typename T> void f(const T& param) {
  std::cout << "T: "     << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl; // show T
  std::cout << "param: " << boost::typeindex::type_id_with_cvr<decltype(param)>().pretty_name() << std::endl; // show param
}

int main() {
  int x = 27;
  f(x); // T: int, param: int const&
}

第2章: auto

  • 型推論によりautoが(ユーザ視点では)変な動きをすることがあるが、諦めて明示的型指定に頼るよりautoを期待通り動かせるようがんばろう。

5: 明示的な型宣言よりautoを使え

  • autoはすばらしい。
    • 初期化しないと型が決まらないので変数の初期化忘れを防げる。
    • C++14ではラムダ式のパラメータにもautoが使える。
    • クロージャを書くときにはstd::function変数に格納するよりauto変数に格納するほうが楽でパフォーマンス的にもよい。
    • size_typeのビット幅の環境依存性(unsigned intと同じとは限らない!)でハマったりすることもautoで防げる。
    • STLコンテナに対するイテレータの型指定誤り(unordered_mapのkey部分にconstをつけ忘れるとか)でうっかり一時オブジェクトが作られまくってパフォーマンス低下とかも防げる
    • コードの冗長性が減ってリファクタリングも楽になる
  • かようにautoは最高だが望まない結果を生むこともあるので注意せよ!

6: autoが望まない型推論をするときは explicitly typed initializer idiom を使え

  • はまるケース
    • 例: std::vector<bool> f(); auto b = f()[0];(fはvectorオブジェクトを返す関数)。std::vector<bool>はビットパッキングをするよう特殊化されており[]演算子はbool&でなくstd::vector<bool>::referenceオブジェクトを返す。実装依存だが、もしf()が返す一時オブジェクトが消えた後にビットフィールドを参照しようとしたら結果は不定!
    • 一般に不可視のproxy classとautoは相性が悪い。マニュアルとヘッダファイルと心の目で不可視のproxy classを見抜け。
  • こうした場合もautoの使用を諦める必要はない。 explicitly typed initializer idiom の出番。ひとことでいうとstatic_castしろということ。前述の例であればauto b = static_cast<bool>(f()[0]);とすればよい。

第3章: Modern C++への移行

  • auto、スマートポインタ、ムーブセマンティクス、ラムダ式、並行プログラミング、C++11/14の大きな新機能。でもそのほかの細かい新機能も大事。

7: オブジェクト作成時に()と{}を区別しろ

  • オブジェクト初期化の書式として、int x(0);, int y=0;, int z{0};がある。(int z={0};も大抵使えるが、ほぼint z{0};と同じ挙動)
    • クラス定義内でのstaticでないデータメンバの初期化は{}と=で書けるが()はエラー
    • コピー不可オブジェクト(std::atomicとか)は{}と()で初期化できるが=はエラー
    • {}での初期化では組込み型での暗黙の精度切り下げ型変換が禁止される。int x(1.0);はOKだが、int y{1.0};はエラー。
    • Widget w();と書いてWidgetクラスのインスタンスwを作ろうとすると関数の宣言になってしまうが、Widget w{};と書けば安心
  • ただし、{}書式はstd::initializer_listを引数型に持つコンストラクタを優先して呼び出してしまう。Tの部分を型変換してでもstd::initializer_listを引数型に持つコンストラクタをその他のコンストラクタより先に探し、その過程で精度切り下げ型変換があるとエラーを吐いてしまう。
    • 空の{}は空のstd::initializer_listとはみなされず、デフォルトコンストラクタが呼ばれる
    • std::vector v1(10, 20);は10要素で、全ての値が20。std::vector v2{10, 20};は2要素で値は10と20。
  • 世の中には{}をデフォルトで使う派と()をデフォルトで使う派がいる。テンプレートユーザがどっち派かわからないので、テンプレートの中でどっちでコーディングするかは難しい問題になる。

8: 0やNULLよりnullptrを使え

  • 0はintでポインタ型ではない。NULLも整数型でポインタ型ではない。nullptrはなんでも型のポインタとみなしてよい。
  • 特にテンプレートやautoと組み合わせて使うときはnullptrを使うと非常によい。
  • (NULLを使ってしまうライブラリの使い手に備えて?)整数とポインタ型のオーバロードは避けろ

9: typedefよりalias宣言を使え

  • なぜならalias宣言はテンプレート化できるから。例:template<typename T> using MyAllocList = std::list<T, MyAlloc<T>>;
  • TMP(テンプレートメタプログラミング)したことがないなら勉強しろ。type traitsというのがある。
    • C++11ではstd::remove_const<T>::typeでTからconst性を取り除くことができるが、C++14ではtypedefでなくaliasを使ってstd::remove_const_t<T>と書けるようになった。ほかにもいろいろなtype traitsがあるよ。

10: スコープなしenumよりスコープ付きenumを使え

  • スコープなしenumでは、名前がenumの外側のスコープに漏れ出す。スコープ付きenumは漏れない。スコープありenumはenum class Color{black, white, red};のように書くだけ。
  • スコープ付きenumでは整数への暗黙変換が行われない。static_castが必須となり、コードの可読性が向上する。
    • C++11ではenumの宣言(定義なし)ができるようになった。グローバルなenumの中に新しい値を定義したときに依存関係で全部のソースが再コンパイルされることを防げる。
    • ただし、enumのメモリ上のサイズはわかってないといけないので、スコープ付きenumは宣言時にサイズが明示的にわかるようになっている。デフォルトではスコープ付きenumはintのサイズと決まっている。また、スコープなしenumもスコープ付きenumも、enum class Status: std::unit32_t;等としてサイズを明示的指定できる。
    • tupleのget等のフィールド指定にはスコープなしenumが便利。スコープありenumだとXのところにはstatic_cast<std::size_t>が必要。std::underlying_typeというtype traitを使ってもよい。

11: プライベートの未定義関数よりdeleteされた関数を使え

  • コピーコンストラクタやコピー代入演算子をライブラリ利用者に使われたくないときのC++98のやりかたはprivate宣言して定義しないことだった。
  • C++11ではpublic宣言して、basic_ios(const basic_ios&) = delete;等と書く。
  • delete記法のメリット
    • 自クラスやfriendからもコピーができないことが保証できる
    • コンパイル時のエラーメッセージがわかりやすくなる
    • 非メンバ関数もdeleteできる: 望まない暗黙型変換による関数呼び出し(bool isLucky(int n); bool isLucky(char) = delete;)や、テンプレートの特定型でのインスタンス化(template typename<T> void f(T* ptr); template<> void f(void*) = delete;)を明示的に禁止できる
    • メンバ関数テンプレートの特定型でのインスタンス化禁止はdeleteでしかできない

12: 他の関数をオーバライドしている関数はoverrideと宣言せよ

  • 関数のオーバーライドが起こるための条件はけっこううっとうしく、書き間違えてもエラーが出ないことが多い
    • 基底クラスの関数はvirtualでないといけない
    • (デストラクタ以外では)関数名が基底クラスと派生クラスで一致してないといけない
    • パラメータ型と、constさと、戻り値と、例外規定も一致してないといけない
    • C++11では、さらにreference qualifierも一致してないといけない。
      • reference qualifierは*thisがlvalueかrvalueかで動作を切り替えるための仕組み (必要になったら本を読み返す)
  • そこで、オーバライドを行う派生クラス側の関数の宣言を以下のようにせよ: virtual void f() override;
    • 何かミスがあったらコンパイラが怒ってくれる。
  • finalというのもある。クラスにも関数にもつけられる。

13: iteratorよりconst_iteratorを使え


* const_iteratorはconst X*と同じく、iteratorの指すオブジェクトをコードが変更しないことを意味する
* C++98ではconst_iteratorは実用的でなかったがC++11では非常に使いやすくなった。

std::vector<int> v;
auto it = std::find(v.cbegin(), values.cend(), 1983); // itの型はconst_iterator
v.insert(it, 1998);
  • テンプレートの中ではstd::cbegin(v)という非メンバ関数呼び出し形が使えるが、C++14でしか動かない。(C++標準化のときの見落としのせいらしい)

14: 例外を吐かない関数はnoexceptと宣言せよ

  • noexcptは関数のインタフェースの一部なので、インタフェースを一旦公開すると呼び出し側コードがそれに依存してしまうことがある
  • noexcept関数は最適化に貢献する。
  • noexceptはmove動作、swap, メモリ解放関数、デストラクタで特に有益。C++11ではデストラクタはデフォルトでnoexcept。
  • 大部分の関数は現実にはnoexcept宣言しない。(例外は投げるかも..という立場)

15: 使える場所ではconstexprを使え

  • constexprなオブジェクト(組込型含む)は定値でかつコンパイル時にわかっている値。
  • constexprな関数は、コンパイル時に判明している定数を与えて呼ばれるとコンパイル時に判明している定数を返す。
  • constexprは関数のインタフェースの一部で、あとで変更すると影響が大きいこともあるので注意。

例:

#include <iostream>
#include <array>

constexpr int pow(int base, int exp) noexcept {
  int out = 1;
  for(int i = 0; i < exp; i++) {
    out *= base;
  }
  return out;
}

int main() {
  std::array<int, pow(3, 3)> ary;
  std::cout << ary.size() << std::endl; // 27
}

16: constメンバ関数をスレッドセーフにしろ

  • メモ化にconstメンバ関数とmutableなメモ用変数を使うのは正当、しかしC++98流の書き方ではスレッドセーフでない
  • mutableなmutexをメンバ追加する:オブジェクト全体がムーブ可・コピー不可になる
  • 1つの変数やメモリ箇所だけに変更を加えるときはatomicなカウンタだけでいい(この場合もオブジェクトはコピー不可になる)

17: 特殊メンバ関数の(自動)生成を理解しろ

  • C++98での特殊メンバ関数: デフォルトコンストラクタ、デストラクタ、コピーコンストラクタ、コピー代入演算子
    • 必要になったときに public inline で自動生成される。デフォルトコンストラクタはコンストラクタが全くないときだけ自動生成される。自動生成コピー演算は要素ごとのコピー挙動。
  • C++11で加わった特殊メンバ関数: ムーブコンストラクタ、ムーブ代入演算子
    • 自動生成されるムーブ関数はデータメンバの要素ごとのムーブを試みる。ムーブ演算がサポートされないデータメンバについてはコピーをする。
  • コピー演算(コピーコンストラクタ・コピー代入)は、2つのムーブ演算がいずれも宣言されていないときだけ自動生成される。(他方の)コピー演算またはデストラクタが宣言されているときのコピー演算の自動生成はC++11ではdeprecated。
  • ムーブ演算の自動生成は、コピー演算・(他方の)ムーブ演算・デストラクタがいずれも宣言されていないときだけ行われる。
  • コピー演算・ムーブ演算・デストラクタのいずれかが宣言されているときのコピー演算の自動生成はobsoleteなので、レガシーコードにWidget(const Widget&) = default;等のコードを追加し明示的にデフォルト実装でよいことを表明せよ。
    • デストラクタをvirtual宣言するだけでムーブ演算の生成が抑制されてしまうのでムーブ演算・コピー演算のサポートを=defaultで明示的に示すのがよい。
    • デストラクタを後で宣言するケースに備えてコピー演算・ムーブ演算をあらかじめ=defaultで明示的に宣言しておくのはよいマナー。
  • メンバ関数テンプレートが、Tを自身のクラスとしてインスタンス化したときに特殊メンバ関数にあたってしまう場合、テンプレートによって特殊メンバ関数が定義されてしまう。 (変換コンストラクタテンプレートからコピーコンストラクタがうっかりできてしまうケースがあることに注意)

第4章: スマートポインタ

  • 生のポインタは強力ではあるが扱いをちょっとでも間違えると痛い目にあう。
  • スマートポインタは生のポインタと似た扱いができ、落とし穴を避けられるのでなるべく使え。C++にはstd::auto_ptr(deprecated), std::unique_ptr, std::shared_ptr, std::uniq_ptrがある。

18: 排他的所有権によるリソースマネジメントにはstd::unique_ptrを利用せよ

  • std::unique_ptrは排他的所有権のセマンティクスをもつ。ムーブ可、コピー不可。破壊時にnon-nullのstd::unique_ptrは自分が指している先のリソースを破壊する。デフォルトではデストラクタが呼ばれるがデリータ関数を指定することもできる。
    • デリータをラムダ式で書くと便利だし性能もよい。(キャプチャをしない関数オブジェクトは関数ポインタのスペースを食わない)
  • ファクトリ関数がstd::unique_ptrを返すのはよくある使いかた。ムーブ演算で所有権を引き継いでいくことができる。
    • ライブラリユーザはstd::unique_ptrを貰えばリソース解放(し忘れや複数回解放、解放方法)について悩まなくてよい。
  • 初期化ずみのstd::unique_ptrに生のポインタの代入はできない。
  • std::unique_ptrはstd::shared_ptrに効率的に暗黙変換できる。(変換は、ポインタが指すリソースの排他所有権管理をやめ共有管理に移行することを意味する)

自作の例:

#include <iostream>
#include <boost/type_index.hpp>

class BaseItem {
public:
  virtual ~BaseItem(){};
};

template<typename T>
class Item : public BaseItem{
  T t;
};

template<typename T>
auto factory() {
  auto customDeleter = [](BaseItem *pItem) {
    std::cout << "Item of type " << boost::typeindex::type_id_with_cvr<T>().pretty_name() << " is being deleted!" << std::endl;
    delete pItem;
  };
  std::unique_ptr<BaseItem, decltype(customDeleter)> pItem(new Item<T>, customDeleter); // decltypeのあたりに注目
  return pItem;
};

int main() {
  auto i = factory<int>(); // auto を使うとよい
  // output: Item of type int is being deleted!
}

19: 共同所有権によるリソースマネジメントには std::shared_ptr を利用せよ

  • std::shared_ptrはリソースのライフタイム管理を自動化するが、GC(Garbage Collection)と違ってタイミングは予測可能。
  • オブジェクトがまだ使われているかを知るために参照カウンタが使われる。
    • std::shared_ptrのサイズは生のポインタの倍。std::shared_ptrオブジェクトにはリソースへの生のポインタのほか、参照カウンタ等を含むコントロールブロックへのポインタも内包されるため。
    • コントロールブロックのためのメモリ領域は動的確保される(典型的にはヒープに)。
    • 参照カウンタのインクリメント・デクリメントはアトミックでなくてはならない。(効率が悪くなりがち)
    • ムーブコンストラクションは参照カウンタを増やさない。(効率的)
  • std::shared_ptrでもカスタムデリータが使えるが、std::unique_ptrと違って型やオブジェクトサイズには影響しない。デリータ情報のための追加領域はコントロールブロックの一部として動的割り当てされるため。
  • コントロールブロックは参照カウントの他にウィークカウント、カスタムデリータ、アロケータ等を含む。
    • コントロールブロックは (1) std::make_sharedが呼ばれたとき、(2) std::unique_ptrやstd::auto_ptrや生のポインタからstd::shared_ptrが作られたとき、に新規作成される。
    • グローバルな重複回避の機構はなく、同一の生のポインタから複数のstd::shared_ptrが作れてしまうので作らないよう注意。カスタムデリータを使わないときはstd::shared_ptrのコンストラクタの代わりにstd::make_sharedを使い、カスタムデリータありのときはstd::shared_ptr<Widget> sp(new Widget, customDeleter);のように1ステートメントで書くとよい。
    • コントロールブロックはいっちょまえに継承とか使っているのでvtableとかも含み、最低3ワードくらいのサイズはある。

20: 指示先が破壊されても構わないstd::shared_ptrとしてstd::weak_ptrを使え

  • std::weak_ptrはstd::shared_ptrのように振る舞うが共有所有権には加わらない(参照カウントに影響しない)。知らないうちに参照先が破壊されている可能性がある。
  • std::weak_ptrはstd::shared_ptrから作る。
  • std::weak_ptr::expired()というメンバ関数で生きているかをチェックできるが、チェックして生きていたら使うようなコードにはアトミック操作の問題が伴う。
  • std::weak_ptr::lock()という関数で参照先が生きている場合だけstd::shared_ptrを作ることができる。参照先が死んでいればnullptrが返る。
  • std::weak_ptrのつかいどころ
    • キャッシュ。オブジェクト生成が高コストで、オブジェクトは再利用可能だが生成されたオブジェクトを永久に残しておくとメモリ容量が大変なケースで「オブジェクトがもしまだ残っていたらキャッシュとして再利用する」戦略をとるのに使う。
    • オブサーバデザインパターン。オブザーバがまだ生きていたら状態変化を報告するが、オブザーバを確実に生かしておきたいわけではないとき。
    • std::shared_ptrで指されている共有されたオブジェクトが「誰から指されているか」を保持したい場合。std::shared_ptrで指し返すと循環参照になってしまう(参照カウンタ型のGCと同じでリソースが回収されなくなってしまう)のでstd::weak_ptrがよい。
      • ただし、循環がありえない階層的なデータ構造だったらstd::weak_ptrをわざわざ使わなくて良い。木構造の親から子へのポインタはstd::uniq_ptr、子から親へのバックポインタとかは生のポインタでOK。

21: newよりstd::make_unique(C++14)やstd::make_shared(C++11)を使え

  • auto upw(std::make_unique<Widget>());のように使う。Widgetのオブジェクトが作られていることに注意。std::make_sharedはC++11で使えるが、std::make_uniqueのほうはC++14でしか使えない。
  • メリット
    • std::unique_ptr<Widget> upw(new Widget);とWidgetを2回書くようなコードの冗長性を避けられる
    • 例外安全性の保証。func(std::shared_ptr<Widget>(new Widget));とか書いたときに、コンパイラ最適化によって生のポインタがスマートポインタに格納される前にfunc()が呼ばれ、例外が発生してリソースリークしたりしないことを保証できる。
    • 効率の向上: newとコントロールブロックのメモリ確保がまとめて1回で行われる
  • 使わないほうがいいとき、使ってはいけないとき
    • カスタムデリータを使うとき
    • std::initializer_list によるオブジェクト初期化が必要なとき (ただしautoをうまく使えば回避できる)
    • (std::make_sharedのみ関係あり)
      • new/deleteで独自のメモリ確保をしているとき
      • メモリに厳しいプロジェクトで、かつ格納オブジェクトサイズが大きく、かつweak_ptrがshared_ptrより長いライフタイムを持つとき (コントロールブロックはweak_ptrがある限り残さないといけないが、セットでメモリアロケートされているとオブジェクト本体部分のメモリもweak_ptrがなくなるまで解放できない)

22: Pimplイディオムを使っているときは特殊メンバ関数を実装ファイルに置け

  • Pimplイディオムはクラスのクライアント(利用者)からクラスの実装を隠すことでビルド時間を短縮する
  • std::unique_ptrでPimplイディオムを実装するときは、ヘッダに特殊メンバ関数を宣言し、実装ファイルで定義せよ。デフォルト実装でOKなときも明示的に宣言せよ。
  • std::shared_ptrを使うと面倒なことになるのでstd::unique_ptrを使え。

第5章: rvalue reference、ムーブセマンティクス、パーフェクトフォワーディング

  • これらは一見スッキリしているようで、深く学び始めると微妙。しかしきちんと理解すれば再びスッキリできるよ。
  • 意識しておけ: 関数の実引数(void f(Widget&& w);におけるw)は、たとえその型がrvalue referenceであろうとも、それ自身は 常に lvalue。

23: std::moveとstd::forwardを理解せよ

  • std::moveとstd::forwardの実態はキャストで、実行コードは生成しない。
  • std::moveは引数を無条件にrvalueにキャストする。 それだけ。
    • std::moveは、引数をmove対象にしてよいという表明を意味するとは限らない。例: constオブジェクトをstd::moveでキャストしても、 constオブジェクトへのrvalue referenceとなるだけ。const オブジェクトはムーブ対象にできないので対象のクラスがムーブ演算をサポートしていても結局コピーが行われる。
  • std::forwardは条件付きキャストで、引数がrvalueに紐づくときだけrvalueにキャストする。

例:

void process(const Widget& lvalArg); // lvalueを処理する
void process(Widget&& rvalArg);      // rvalueを処理する

template <typename T>
void logAndProcess(T&& param) {
  // logging code blah blah blah
  // ...
  process(std::forward<T>(param)); // logAndProcessがrvalueを与えて呼ばれたときだけparamをrvalueにキャストする
}

24: universal referenceとrvalue referenceを区別せよ

  • T&&には、使われ方によって2つの異なる意味がある。
  • 型推論を伴うT&&の使用とauto&&はuniversal reference。ただしT&&についてはぴったりこの記法どおりの場合のみ。 constとかTに何かくっついていたり、テンプレートパラメータではあってもそこで型推論がされない場合はuniversal referenceでなくrvalue referenceとなる。
  • 上記条件以外はすべてrvalue reference。
  • universal referenceはrvalueで初期化されればrvalueを指すものとなる。lvalueで初期化されればlvalueを指すものとのなる。
  • universal referenceというのは実は方便で、文法上は実際は存在しない幻。(see #28)
void f(Widget && param);   // rvalue reference
Widget && var1 = Widget(); // rvalue reference
auto && var2 = var1;       // universal reference
template<typename T> void f(std::vector<T>&& param); // rvalue reference
template<typename T> void f(const T&& param); // rvalue reference
template<typename T> void f(T&& param); // universal reference
template<class T, class Allocator = allocator<T>>
class vector {
public
    void push_back(T&& x); // rvalue referene
    template <class... Args>
    void emplace_back(Args&&... args); // univerrsal reference
};

25: rvalue referenceにはstd::moveを、universal referenceにはstd::forwardを使え

  • rvalue referenceはムーブ対象としてよいオブジェクト(抜け殻にしてよいオブジェクト)への参照。
  • std::forwardをrvalue referenceに適用するのは、動作上は問題ないが読み手の混乱を招くからやめておけ。
  • std::moveをuniversal referenceに適用するのは絶対だめ。
  • そもそもuniversal referenceなんかなきゃいいじゃないか、と思うかもしれないが、以下の必然性があるのだ。
    1. 余分な一時オブジェクトを作らないですむケースがある、
    2. 複数または不定数個)の引数のlvalueとrvalueの2^n個の組み合わせに対するオーバロードをすべて書くのは嫌(不定数個では不可能)
  • std::moveやstd::forwardはその参照の最後の利用時にだけつける。(まだ次回使うのに対象のオブジェクトが抜け殻にされてしまっては困るから)
  • 値渡しでreturnする関数でrvalue referenceやunversal refernceをreturnするときもstd::moveやstd::forwardをつけろ。
  • だからといってローカル変数を値渡しでreturnするときにstd::moveとかつけるな。RVO (Return Value Optimization)に任せろ。

26: universal referenceについてのオーバロードを避けろ

  • universal reference引数の関数をオーバロードしたとき、universal refernce版のほうの関数がほとんどのケースで呼ばれてしまう。(暗黙型変換やconstの追加がないと非universal reference版の関数が呼ばれないときはuniversal reference版が優先されてしまう。)
template<typename T> void f(T&& name);
void f(int x);
short i = 27;
f(i); // int 版でなはく universal reference版が呼ばれる
  • perfect forwardingコンストラクタは特に問題があり、non-constなlvalueを与えて呼び出そうとしたときにコピーコンストラクタより優先されてしまう。そのため、派生クラスでからの基底クラスのコピー・ムーブコンストラクタの呼び出しにperfect forwardingコンストラクタが割り込んでしまう。 (perfect forwardingコンストラクタは必ずテンプレートなので、コピーコンストラクタとは異なる挙動であるはずで、結果問題を招く)
    • const のついた引数を持つコピーコンストラクタより、constのついてないテンプレート引数を持つperfect forwardingコンストラクタのほうが優先度が高い
class Person{
public:
  template<typename T>explicit Person(T&& n) : name(std::forward<T>(n)) {}
   // perfect forwarding constructor: std::stringなどいろんな型で名前を与えてPersonを初期化する意図
   Person(const Person& rhs); // コピーコンストラクタ (コンパイラ生成)
   Person(Person && rhs); // ムーブコンストラクタ (コンパイラ生成)
   ...
};
Person p("Nancy");
auto cloneOf(p); // コンパイルエラー: コピーコンストラクでなくperfect forwardingコンストラクタを呼んでしまう

27: universal referenceについてのオーバロードの代替方法を知っておけ

  • オーバロードを諦める: 問題外
  • const T& 渡しにする: C++98時代に戻り、perfect forwarding を諦める
  • 値渡しにする
  • tag dispatchを使う
template<typename T> void logAndAdd(T&& name) {
  logAndAddImpl(std::forward<T>(name),
                std::is_integral<typename std::remove_reference<T>::type()); 
};

template<typename T> void logAndAddImpl(T&& name, std::true_type); // T が整数型のときに呼ばれる
template<typename T> void logAndAddImpl(T&& name, std::false_type); // それ以外のときに呼ばれる
  • universal referenceを受け取るテンプレートに制約を加える
class Person{
public:
    template<
        typename T,
        typename = typename std::enable_if<
            !std::is_same<Person,
                typename std::decay<T>::type>::value
            >::type
    >
    explicit Person(T&& n);
    ...
};
  • perfect fowardingは性能上有利だが、#30に示す利用条件の制約があるほか、コンパイラのエラーメッセージが分かりにくくなる(forwardされた先でエラーが出るため)

28: reference collapsing(参照の縮約?)を理解せよ

  • #24で説明したuniversal referenceというのは方便で、実体はやっぱりrvalue referenceでした。
    • template<typename T> void func(T&& param);におけるTの型推論結果は、引数にlvalueが渡されたかrvalueが渡されたかが反映される: lvalueが引数に渡されたらTはlvalue reference、rvalueが渡されたらTはnon-reference。 (see #1)
    • その上で、reference collapsingという機構がuniversal refernceという幻を作っている。
  • reference collapsingは「参照の参照」をひとつの参照に縮約する働き。2つの参照がいずれもrvalue referenceの時だけ縮約された参照はrvalue refereneになり、それ以外の場合はlvalue referenceになる。
  • reference collapsing は次の4つのいずれかの場合でだけ起こる: (1) テンプレート具現化、 (2) auto 型生成、 (3) typedef と alias の宣言、 (4) decltype。
  • このように、universal referenceというのは実態としては存在しないのだが、 universal referenceがあると思っていても挙動を誤解するわけではなく思考を単純化できる便利な概念。

29: move 操作は存在しないこともあるし、効率的でないこともあるし、使われないこともある

  • C++11のムーブセマンティクスは過信されがちで、「コンテナをムーブするのは生のポインタ1つをコピーするくらい効率的だ」とか「C++98のコードを単にC++11で再コンパイルするだけでコピー操作が自動的にムーブ操作に置き換えられて性能が勝手に上がる」とか思われているが、いろいろ例外はあるので過信するな。 (注: 過信を諌めたいだけでムーブセマンティクスなんて無価値だと言っているわけではないと思う)
  • #17で説明したように、ムーブ操作が自動生成されないケースがある。
  • ムーブ操作がコピー操作と同等程度の性能で、別にメリットがないケースがある。
    • std::arrayは、格納対象のオブジェクトへのポインタの配列をヒープに確保するではなく自身のオブジェクトの中に持つ。したがって、std::arrayのムーブはarrayサイズに対し線形時間かかる。
    • 短い文字列のバッファも同じくオブジェクトの中に置かれることが多い(SSO: Small String Optimization)。同じくこのときもコピーしてもムーブしても効率は変わらない。
  • STLのコンテナ操作のいくつかは強い例外安全性を保証している。これらの中では、ムーブ演算が例外を投げない保証があるときだけコピーがムーブに置き換えられる。(つまり、ムーブ演算を定義しておいても使ってくれないケースがある)
  • 前提がきっちり満たされていればムーブ演算は間違いなく使われると思っていいよ。

30: perfect forwardingが失敗するケースを知っておけ

  • まずperfect fowardingというのは、「受け取った引数をそのまま別の関数にフォワードすること」。オブジェクトをコピーすることもないし、lvalue/rvalueの別もCV修飾子も保存される。便利だね!

  • perfect forwardingがうまく働かない例外

    • {}初期化子
    • 0やNULLをポインタ扱いして与えたとき (see #8)
    • 宣言しかしてない static const 整数型データメンバ
      • 定義がないと static const 整数型データメンバはメモリ上に実体を持たないので参照が作れない。これは、定義を実装ファイルに書くだけで解決できる。
    • オーバロードされた関数名や、テンプレート名
      • どのバージョンが呼ばれるかわかってない時点では、これらは関数ポインタではなく「名前」にすぎないのでperfect forwardingがうまくいかない。
      • alias とか使ってバージョンを明示的に指定することで解決できるらしい (欲しくなったら本を見ること)
    • ビットフィールド
      • ビットフィールドへの参照は作れないので引数に与えるとコピーが生成される。あらかじめstatic_castすればOK。

第6章: ラムダ式

  • ラムダ式は強力。STLアルゴリズムのpredicateとか、sortの比較関数とか、std::unique_ptrやstd::shared_ptrのカスタムデリータとかが簡潔に書ける。
  • 用語整理。
    • ラムダ式は単なる式。ソースコードの[...](...){...}部分そのもの。
    • クロージャはラムダ式によって作られたランタイムのオブジェクト。キャプチャモードによって外部変数への参照を保持したりコピーを保持したりする。std::find_ifとかをラムダ式を与えて記述したとき、ランタイムで関数の引数として渡されるオブジェクトはクロージャ。
    • クロージャクラスはクロージャ(=インスタンス)が属するクラス。ラムダ式はそれぞれ独自のクロージャクラスをコンパイラに生成させ、ラムダ式の中に書いた記述はクロージャクラスのメンバ関数になる。

同一クラスのクロージャのコピーとかもできる。

{
  int x;
  ...
  auto c1 = [x](int y) {return x > y > 55; };
  auto c2 = c1; // クロージャのコピー
}

31: デフォルトのキャプチャモードは避けろ

  • デフォルトキャプチャモードとは[=]とか[&]のこと。
  • キャプチャはラムダ式が書かれたスコープで見えているnon-staticなローカル変数に対してのみ起こる。
  • 参照モード([&])でのキャプチャには、対象変数がクロージャより先にスコープ外になって消えてしまうリスクがある。
  • コピーモード([=])でもポインタをキャプチャしてしまうとポインタの指示先が消えてしまうリスクがある。(とくにthis。[=]モードは暗黙のうちにthisを値としてキャプチャしてしまう)
  • キャプチャは[hoge]とか変数名を明示的に書いて行え。

32: オブジェクトをクロージャ内にmoveするにはinit captureを使え

  • C++14で導入されたinit captureではデータメンバ名とデータメンバの初期化式を記述できる。
auto pw = std::make_unique<Widget()>;
auto func[pw = std::move(pw)]{ ...}; // 左辺のpwはクロージャクラスのデータメンバとなる
auto func2[pw = std::make_unique<Widget()>]{...}; // こんなふうにも書ける
  • C++11でも、std::bindをうまく使えば同じことができる (詳細略)

33: auto&& 型のパラメータを(lambda内で)std::forwardするときはdecltypeを使え

  • C++14ではパラメータをauto指定することで強力なジェネリックラムダ式が使える。
  • ラムダ式の引数をラムダ式の中で呼び出す関数にパーフェクトフォワーディングしたいときは以下のように書けばよい:
auto f = [](auto && param) {
  return func(std::forward<decltype(param)>(param));
};

34: std::bindよりラムダ式を使え

  • std::bindよりラムダ式のほうが見やすくかつ効率的らしい。std::bindの理解が前提になっているが、そもそもよく知らないので詳細はパス。

第7章: 並行プログラミング(concurrency)API

  • 標準ライブラリにマルチスレッディングが組み込まれているのはすばらしいことだよ
  • futureにstd::futureとstd::shared_futureとがあるが、ほとんど同じなので総称してfutureと呼ぶよ

35: スレッドベースよりタスクベースでプログラミングせよ

int doAsyncWork();
std::thread t(doAsyncWork); // スレッドベース
auto fut = std::async(doAsyncWork); // タスクベース
  • std::asyncから得られるfutureはget関数を持ち、doAsyncWorkの戻り値や吐いた例外が明示的にわかる。
  • std::asyncはスレッドを即時に起動する保証はなく、スケジューラに起動のタイミングやHWスレッドの割り当てを任せる。 (OSスレッド数の上限やコンテクストスイッチによるキャッシュ効率低下に関わるマネジメントを標準ライブラリにお任せできる)
  • GUIスレッドの応答性とかはなお問題なので#36で議論する。
  • std::asyncを使うべきでない例外的状況
    • pthread とか Windows' ThreadsとかのネイティブなAPIにアクセスしたいとき
    • スレッドの使い方を自分のアプリに最適化したい(+自分にその能力がある)とき
    • C++の並行プログラミングAPIの能力を超えることをやる必要があるとき

36: 非同期実行の必然性があれば std::launch::async を指定せよ

  • std::async でのスレッド起動ポリシーは「同期で(自スレッド内で)実行してもいいし非同期で(別スレッドで)実行してもいいよ」となっている。この自由を使ってロードバランスとかを標準ライブラリ側ががんばってくれる。
  • 明示的に指定したい場合は以下の2つが用意されている。
    • std::launch::async : 絶対別スレッドで実行しろ
    • std::launch::deferred : std::asyncが返したfutureに対してgetかwaitが呼ばれたときにだけ実行しろ。getかwaitが呼ばれたときには自スレッドで実行しろ
    • デフォルトの起動ポリシーでは実際にどっちのポリシーが採用されたかわからないので、そもそも実行対象の関数が実行されたかどうかすら不明のケースもある (その後getやwaitしないときはdeferredモードが選択されていると実行されずじまいになる)
    • deferred モードで実行されているときはwait_forはいつもstd::future_status::deferredを返すので注意。戻り値がstd::future_status::readyになるのを待っていると無限ループになる。
  • wait_for(0)でモードを調べて処理をスイッチする方法もある。どうしても非同期実行でないといけないときだけstd::launch::asyncを指定しろ。

37: std::thread が全ての実行パスでunjoinableになるようにせよ

  • unjoinableなstd::threadオブジェクトとは、以下のいずれかの状態: (1)デフォルトコンストラクタで生成されていてスレッドに紐付いていない、(2)moveされて抜け殻、(3)joinずみ、(4)detachずみ
  • std::threadオブジェクトが破棄される前にunjoinableにしておかないとプログラム全体が強制終了されてしまう(スレッドが、ではない!)。
  • std::threadを包むRAII(Resource Acquisition Is Instantiation)オブジェクトを作り、RAIIオブジェクトのデストラクタにおいて、生成時にあらかじめ指定しておいた方法(join or detach)でstd::threadをunjoinableにするようにRAIIクラスを書いておくとよい。
  • ThreadRAII クラスの中で std::thread は最後のデータメンバとして記述すること。 (書いた順に初期化されるので、スレッドが起きたときには他のデータメンバが初期化ずみであることが保証される)

38: std::threadとfutureオブジェクトの破壊時の挙動の差を知っておけ

  • joinableな状態にあるstd::threadオブジェクトとfutureオブジェクトはいずれもOSスレッドへのハンドルとみなせる。
  • futureオブジェクトの破壊時の挙動はちょっと複雑。std::threadと違ってプログラムの強制終了は起こらない。
  • futureはstd::shared_futureとしてコピーできる。呼び出された関数の状態と演算結果は呼び出し元・呼び出し先いずれのスレッドにも属さない共有領域に置かれる。コピーされたfutureの間ではこの共有領域は共通で、std::shared_ptrと同じように参照カウンタで管理される。
  • std::asyncによって作られたひとつの共有領域に紐付いた最後のfutureオブジェクトが破棄されるとき、非同期指定(std::launch::async)で最初のスレッド起動がされていれば暗黙joinになる(対象スレッド終了まで呼び出し元スレッドがブロックされる)。それ以外のときは、ただfutureオブジェクトが破棄される(すでにスレッドが非同期実行されていればdetachに相当する。もしくはタスクが同期型で遅延実行予定だったとすると全く実行されずに終わる)

39: 1回きりのイベント通信には void future の利用を考えよ

  • スレッド間のイベント通知の方法
  • std::condition_variableをロック用のstd::mutexと一緒に使うのがわかりやすい方法。しかし、イベント送信側のタスクがnotify_one(またはnotify_all)する前にイベント受信側のタスクがwaitしはじめていないといけない制約がある。また、spurious wakeup(condition_variableの条件が満たされていないのになんか受信側タスクが起こされてしまう)に対処することができない。受信側タスクは送信側タスクがイベント通知の前提条件を満たしたかどうかを観察できないから。
  • std::atomic でフラグを作って、送信側はflag=true;とし、受信側はwhile(!flag);としてもいいがこれはポーリングで効率に問題がある。
  • フラグと条件変数(condition variable)を併用するのがよくやる方法だが、mutexによるフラグの保護も必要で、課題は解けるもののゴチャゴチャする。
  • 次の方法がある:送信側タスクがstd::promise<void> p;としてオブジェクトを作り、p.set_value();してイベント送信をする。受信側はp.get_future().wait();すればよい。ただヒープに共有状態のための領域がとられるのと、std::promiseオブジェクトが1回きりしか使えない問題がある。

40: std::atomicは並行プログラミングに使い、 voatile は特殊なメモリに使え

  • 世間で誤解されているようだがvolatileと並行プログラミングは関係ない。
  • std::atomicはmutexでロックされているかのように、read, write, RMW(read-modify-write操作)を分割不可能な(atomicな)操作として、CPU機能を使って効率的に実現できる。
  • std::atomicを使うことで、statementの間の順序の保証ができる。たとえば、下記コードで flag が true になったことは func() の実行が完了したことを保証する。 flag がただの bool だったら保証されない。
std::atomic<bool> flag(false);
int x = func();
flag = true; // CPUによる最適化も含め、xの値が更新されてからflagが更新されることが保証される
  • じゃあvolatileの意味は?: 対象が「特殊なメモリ」だとコンパイラに教えること。10を書き込んだ直後に20を書き込むことは無意味に思えるが、Memory Mapped IOとかだと意味がある。また、自分は書き込んでいないのに値がいつのまにか変わっていることもありえる。という前提に立って、読み込みや書き込みは言われた通りにやって変な最適化するなよ、というコンパイラへの指示がvolatile。

  • volatile std::atomic val; みたいなケースもありえる。

第8章: 補遺

  • 値渡しと emplace は使いどころを間違えやすいので補足するね。

41: 引数がコピー可で、受け取り側で常にコピーが必要で、かつ効率的にムーブできるときは値渡しも考えろ

  • パラメータを受け取って*thisの中のコンテナに格納するようなメンバ関数を書くときで、lvalue版のコピーとrvalue版のムーブと2つ関数を書くケースあるよね。面倒くさいよね。
  • universal referenceで1つの関数にできるけど、テンプレートになるからヘッダに実装を置かないといけないとか、オブジェクトコードのサイズが膨れ上がるかもとか、渡せない引数型がある(#30)とかコンパイラのエラーメッセージがわかりにくい(#27)とかいろいろ面倒だよね。
  • 値渡しで受け取ってstd::moveをつけてコンテナに入れればいいんやで。値渡し時のとき、C++98では常にコピーコンストラクタが呼ばれるがC++11では引数がrvalueならムーブコンストラクタが呼ばれる。
class Widget {
public:
    void addName(std::string newName) { // 値渡し
        names.push_back(std::move(newName)); // std::moveに注目
    }
private:
    std::vector<std::string> names;
};
  • 値渡し解法では、オーバロードと比べてもuniversal referenceと比べても、引数がrvalueであれlvalueであれムーブが1回余計に行われるので、この解法がOKなのはムーブが効率的な場合に限る。
  • ムーブ演算不可な場合、ムーブの代わりにコピーがされるので非効率。この場合値渡しはよくない。
  • あと、メンバ関数内に条件判断があってコピーを結局しない可能性があるなら、値渡しで作ったコピーが無駄になるので値渡しはよくない。
  • ほんとうに値渡しで効率が落ちないかどうかはその他いろいろ条件があるのでよく考えろ。コピー代入よりコピー初期化のほうがコストが高いケースもある (長い文字列の入っている std::string を短い文字列に書き換えるときとか)
  • 値渡しはスライシングの問題を伴うので派生クラスを基底クラス型の変数に値渡し代入する際には気をつけろ

42: insert(push_backとかの総称)の代わりにemplace(emplace_*の総称)の利用を考えろ

  • コンテナに値を挿入するときに一時オブジェクトが作られるとすると、ムーブ演算が余分にかかる。emplaceを使えば一時オブジェクトの生成を避けられる。
std::vector<str::string> vs;
vs.push_back("xyzzy"); // 実態はvs.push_back(std::string("xyzzy"));
vs.emplace_back("xyzzy"); // ベター
vs.emplace_back(50, 'x'); // xが50回続く文字列をvsに挿入する
  • じゃあinsert使うのをやめていつもemplaceを使えば?と思いきや、insertのほうが性能がいいケースもある。コンテナの種類など前提条件が複雑なのだが、下記条件がすべて満たされれば、ほぼemplaceのほうがよい。
    • 値がコンテナ内で代入されるのではなくオブジェクトが新しく構築される(push_backとかに相当する状況)
    • 挿入される引数型がコンテナに格納される型と異なる (insertだと一時オブジェクトが作られる)
    • コンテナが挿入しようとした値を重複として拒絶する可能性が低い (重複をチェックするコンテナではemplaceは比較のために一度オブジェクトを作ってみて、重複が検出されたら破棄することになる)
  • std::shared_ptrをコンテナに格納し、かつカスタムデリータを使うときはemplaceではなくpush_backで、ptrs.push_back(std::shared_ptr<Widget>(new Widget, customDeleter));のようにせよ。shared_ptrを作ってからコンテナに入れに行くという順序を確保すると例外安全的にベター。一般にnewとかをemplaceに渡すべきでない。
  • explicitコンストラクタを伴うときは要注意。emplace_backの中ではコンストラクタが明示的に呼ばれる。あと、regexは const char*を受け取るexplicitなコンストラクタを持つ。
std::vector<std::regex> regexes;
regexes.emplace_back(nullptr); // 不正だがコンパイルエラーにならない
regexes.push_back(nullptr); // コンパイルエラー
std::regex r = nullptr; // コンパイルエラー
std::regex r(nullptr); // コンパイルエラーにならない
237
248
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
237
248