graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

"Effective Modern C++" を読んだメモ。

週末に届いたので読みながら思ったことのメモ。

O'Reilly Japan - Effective Modern C++
『Effective Modern C++』勉強会まとめ - Cybozu Inside Out | サイボウズエンジニアのブログ

1章 型推論

項目2
  • そういや const auto って宣言できるよね、C#const var ができないからつい忘れがち
  • uniform initialization、なんかちょっと不思議なかんじ → 項目7

2章 auto

項目5
  • 書いたとおりの型がちゃんと使われるから auto を使うのがいいという話
    • まさに例示されてた size_t 周りで嵌ったことあるけど、あれは auto じゃなくてそもそも暗黙の型変換に頼った自分が悪い
項目6
  • auto で受ける前に static_cast するやつ、ETTI と呼ぶらしい、手法に名前があるってだいじ

現代C++への移行

項目7
  • {} で初期化するとstd::initializer_list のオーバーロードが呼ばれる
    • 個人的には () 派
項目10
  • UserInfoFields のケースは unscoped enum より整数型定数を使いたい、サイズは少し増えるけど
    • ていうか std::tuple にエイリアスを貼るのは正直ちょっとどうかと思う
項目11
  • 関数定義の delete は、実装されたくないオーバーロードや引数の暗黙な変換を防ぐためにも使える、なるほど
項目13
  • 外部に cbegin, cend を実装する方法、へぇーー
    • template auto cbegin(const T& t)->decltype(std::begin(t)) { return std::begin(t); }
項目14
  • 「関数は事前条件が満たされていると想定することが認められている」、そりゃそうだって感じだけど改めて言われるとちょっと怖い
項目15
  • 「値の const なコピー」と「コンパイル時に値が決定されている」は違う、うん
  • constexpr 関数は "コンパイル時に既知な値を実引数に与えられれば"、コンパイル時に使用可能な値を返す。なにこれ超おもしろい
    • 与えられなければふつうにランタイムで値を返す
項目17
  • Rule of Three
  • ムーブ演算が自動生成されないケース、全然しらなかった。。
    • クラスがコピー演算を宣言している
    • クラスがムーブ演算を宣言している
    • クラスがデストラクタを宣言している
    • ※ ただしテンプレートクラスは除く
  • ムーブ演算を宣言した場合はコピーコンストラクタが生成されない
項目18
  • std::unique_ptr のカスタムデリータにキャプチャなしラムダを使うときは unique_ptr インスタンスのサイズは増えない
    • Empty Base Optimization が効いてるはず
    • 関数ポインタの場合はそのぶんサイズが増える、気をつける

4章 スマートポインタ

項目19
  • std::shared_ptr がコントロールブロックを作成するケース
    • std::make_shared
    • std::unique_ptr, std::auto_ptr からのコンストラクト
    • 生ポインタからのコンストラクト
項目21
  • std::make_shared は内包するオブジェクトとコントロールブロックを一括で確保する
    • 両者の生存期間が一致する
    • カスタム new/delete は使えない
項目22
  • std::unique_ptr はデリータ定義も含めた型なので、これに収めるクラスはデストラクタを持つ必要がある
    • unique_ptr を解放するには、コンパイラが Impl のサイズを知る必要がある
    • .h の時点では Impl は不完全型なので、~Impl を完全形として扱える .cpp 側に ~Impl を記述する必要がある
      • ただし unique_ptr なので実際の解放処理自体を記述するないため、Impl::~Impl() = default で十分
    • pImpl を破棄する必要のあるムーブ演算についても同様
    • コピー演算が必要な場合は自前で記述する
      • unique_ptr が noncopyable なのでデフォルトコピー演算子は定義されない

5章 右辺値参照、ムーブセマンティクス、完全転送

項目23
  • constオブジェクトに対するムーブ要求はコピー演算に変換される
項目24
  • 型推論を伴う type&& → ユニバーサル参照
    • template void emplace_back(Args&&... args) とかみてると型パラメータ制約が欲しくなるなぁ
    • 変な型いれたらコンパイルで弾かれるんだから別にいらんだろってのはわかる、けど、見た目にやさしくないしエラーもわかりにくい
  • 型推論を伴わない type&& → 右辺値参照
項目25
  • Return Value Optimization (RVO) が可能なケースでは、コピーの省略か戻り値の move のどちらかが必ず実行される
    • return std::move(variable) とかやってコンパイラの邪魔をしてはいけない
項目28
  • ユニバーサル参照の展開
    • 右辺値参照の右辺値参照は右辺値参照
    • それ以外は左辺値参照
項目29
  • ムーブ演算に対応してないクラスは意外と多いことを覚えておく
項目30
  • 完全転送がエラーになるケース
    • 型推論ミス
    • 型またはポインタが確定しない

6章 ラムダ式

項目31
  • ラムダを使うときはキャプチャ対象を明示する
    • プログラマとしての良心とかそっち系の話題?
    • Modern C++ Coding Standards の出版はよ
項目32
  • C++11 でも std::bind 経由で初期化キャプチャ相当の操作ができる
    • std::bind([](const Data& data) {...}, std::move(data));
    • std::bind([](Data& data) mutable {...}, std::move(data));
    • C++14 では auto f = [data = std::move(data)]() { ... } で OK

7章 並行API

項目36
  • std::async のデフォルト実行ポリシーは std::launch::async | std::launch::defered
    • そのおかげで、オーバーサブスクリプションやスレッド枯渇が起こることはない
      • ただしスレッドプールは実装されていない
    • async したスレッド上で同期的に遅延実行される可能性がある
      • async の戻り値を get/wait していなければ実行されず、wait_for で無限ループに嵌まる可能性がある
      • 遅延実行されたかどうかは if (wait_for(0s) == std::future_status::deferred) で判定可能
    • どのスレッドで実行されるか確定できないため、スレッドローカルストレージを使うときは気をつける
  • 場合によっては明示的に std::launch::async を指定するのも有効
項目37
  • std::thread のデストラクタは、その時点でスレッドが joinable でなければプログラム全体を停止させる仕様
    • 暗黙に join すると予測不可能な待ちが発生する可能性がある
    • 暗黙に detach すると不正アドレスを読み書きしてしまう可能性がある
      • 例えば thread を抱えるインスタンスがデストラクトされた場合など
    • RAII でデストラクト時の挙動を制御するのも有効
項目38
  • std::promise は共有ステートに結果を書き込み、std::future はそこから読み込む
    • shared_future を使うと共有ステートの参照カウンタがあがる
  • 非同期実行されたタスクの結果を保持する std::future はデストラクト時に実行スレッドを暗黙に join する
    • shared_future で複数参照されている場合は、最後の future のデストラクタが join する
項目39
  • 一度しか notify しない std::condition_variable を使うなら、std::promise を使うのがいい
    • std::promise p; p.set_value(); でシグナル化
    • p.get_future().wait() でシグナル待ち
  • メリット
    • コードがシンプルになる
    • シグナルが来る前に wait() しても暴走しない
    • spurious wakeup しない
  • 注意点
    • promise の共有ステートはたいてい動的確保される
    • 確実に set_value() しないと待ちスレッドが永遠に待ち続ける
項目40
  • std::atomic
    • R/W を含む一連の命令発行順序は最適化されても入れ替わらない
    • 不要な R/W が最適化で消される可能性がある
    • std::atomic::load, store なら重複しても消されないけど、load,store を含む文全体がアトミックにはならない
  • volatile
    • R/W を含む一連の命令発行順序が最適化で入れ替わる可能性がある
    • 不要な R/W は最適化されても消されない

8章 もう一ひねり

項目41

  • 値渡しでも、参照渡しと同程度のコストが期待できる条件
    • 仮引数が関数内で常にコピーされる
    • コピー可能
    • ムーブのコストが十分に小さい
    • 仮引数がコピーコンストラクトされない
    • スライシングを起こさない

項目42

  • → 項目21
    • リソースの new と管理オブジェクトの作成タイミングが異なる場合は、emplace すると例外安全性が低くなる可能性がある
    • 管理オブジェクトまで作成してからコンテナ内に完全転送されれば問題ない
  • emplace 系に与える仮引数の型によっては explicit コンストラクタが呼び出される
    • コンストラクトの引数を渡しているということを意識しておかないと、意図しない挙動になる可能性がある