boost.勉強会#19東京 effective modern c++とc++ core guidelines

133
Effective Modern C++C++ Core Guidelines okada(@okdshin)

Upload: shintarou-okada

Post on 23-Feb-2017

7.049 views

Category:

Technology


4 download

TRANSCRIPT

Effective Modern C++とC++ Core Guidelines

okada(@okdshin)

C++を書くのに必要な知識規格?

(引用元 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3797.pdf)

C++標準規格から分かることC++の文法

C++の機能

C++標準ライブラリの仕様

規格は何を考慮してコードを書けばいいのかは教えてくれない

感覚としては辞書に近い

C++11/14で関数はどのように書くべきなのか?

クラスはどのように書くべきなのか?

{}による初期化で気をつけるべきことは?

いつForwarding Referenceを使うべきなのか?

......

C++11/14でいろいろな機能が追加されたのはなぜなのか?

どれもきちんとした理由がありその理由に応じた適切な使い方がある

しかし規格だけでは分からない

そこでEffective Modern C++

(引用元 https://www.oreilly.co.jp/books/images/picture_large978-4-87311-736-2.jpeg)

Effective Modern C++Effective C++の著者 Scott Meyers氏によって書かれた

C++11/14の解説書

9月に邦訳も出ました「C++11/14プログラムを進化させる42項目」

(引用元 https://www.oreilly.co.jp/books/images/picture_large978-4-87311-736-2.jpeg)

僕がなぜここにいるのかEMC++邦訳の査読をしました。

査読するまでの経緯

査読って何したの?

報酬は?

EMC++第一章「型推論」まるまるPDFで公開されている

http://www.oreilly.co.jp/pub/9784873117362/ch01.pdf

……

EMC++が手に入らない場合EMC++勉強会@東京の資料が公開されている

https://github.com/herumi/emcjp

Q. Effective Modern C++以外にC++11/14の書き方を指南するよう

な書籍はないの?

A. あります。

C++ Core Guidelineshttps://github.com/isocpp/CppCoreGuidelines

著者はBjarne Stroustrup氏とHerb Sutter氏

C++ Core Guidelinesって?

「The C++ Core Guidelines are a set of tried-and-trueguidelines, rules, and best practices about coding in C++」

「C++コーディングにおける実証済みのガイドラインとルール、そしてベストプラクティスの集合体である」

Q. C++ Core Guidelinesは"More" Effective Modern C++か?

A.どちらかというとNoModern C++ Coding Standardsと言

うべき印象

(引用元 http://www.gotw.ca/images/c++cs-cover.jpg)

内容もCCSと結構被ってるところがある

書き方もそっくり

EMC++と比べて項目が短くて簡潔

C++ Core Guidelinesはまだ未完成アプリケーションやライブラリの設計に対するルール集

であり、C++のチュートリアルではない。

複数のコードベースをまたいでより統一的なC++コードスタイルを確立することを目指す。

基本的に新しくコードを書くことを前提にしている。

内容が直感、経験に反するかもしれない。修正歓迎。

C++ Core Guidelinesは実用性重視なるべくたくさんのルールを入れる。

内容的な重複も厭わない

一見、とるに足らないようなルールもある。(初心者がいることを忘れるな!)

完璧に合法なC++のためにエラーが起きやすいコードを書くようなことは勧めない。

C++のサブセットでJavaを作ったりはしない。

将来的に解析ツールでルールが守られているか自動でチェックでき

るようにするプログラマがルールを全部知っておく必要はないように

したい

解析ツールが参照しやすいようにルールにインデックスを付ける(例:F.15、ES.30)

ここからはC++ Core Guidelinesから僕が独断と偏見で

選んだルールを紹介

ところで、関数書いてますか?

C++に新たに導入されたものども

スマートポインタ(uniqur_ptrとshared_ptr)

unique_ptrは排他的所有権(exclusive ownership)を扱う

shared_ptrは共同所有権(shared ownership)を扱う

Rvalue referencevoid f(T&& t) { // T&&がrvalue reference ...}

Forwarding reference*template<typename TP>void f(TP&& t) { // TP&&がforwarding reference ...}

Forwarding referenceについてはEMC++項目24を参照* EMC++ではユニヴァーサル参照(universal reference)となっている。

Q. C++11/14では関数の引数や返り値はどのようにすればいい?

A. これまで通りで全く大丈夫

F.15 なるべくこれまでどおりのシンプルなやり方で情報を渡す

「特殊で賢いテクニック」は他のプログラマを困惑させ、コードの理解を妨げ、バグの温床となる。

どうしてもよく知られたやり方以外の方法が相応しいと思うなら、性能が実際に向上することを計測によって確かめ、きちんとドキュメント化しておく(おそらく可搬

性がないため)。

(引用元 https://github.com/isocpp/CppCoreGuidelines/blob/master/param-passing-normal.png)

Q. 引数はスマートポインタで受け取るべき?

A. 関数の処理に所有権がからむかによる。

F.7 一般的な用途では、スマートポインタではなく

T*で引数を受け取るR.30 ライフタイムに関連する操作を行う場合にのみ、スマートポイ

ンタを仮引数とする

スマートポインタで引数を受け取るようにしてしまうと、関数の利用者に対してスマートポインタを使うこと

を強制してしまう。

std::shared_ptrなどの一部のスマートポインタは実行時コストが大きい。

Q. ムーブセマンティクスでvectorとか、巨大なオブジェクトをそのまま返り値にしても良くなったっ

て聞いたけれど?

A. はい。しかし例外事項には注意が必要。

F.20 出力用の仮引数を用意するよりも、なるべくreturnで返す。

F.21 複数の値を出力したい場合はなるべくtupleやstructを使う。

入出力用引数は出力専用引数と混同されやすいが、返り値は問題ない。

返すオブジェクトがSTLコンテナであっても、暗黙的にmoveされる上に、

明示的にメモリ管理する必要がなくなる。

ただし、個別にはムーブコストが安いオブジェクトもそれがたくさん集まった構造体ではコストが累積して高く付くかもしれないことに注意。(例外事項を参照)

例外事項値型でない場合、例えば派生クラスを基底クラスのポインタで返したい場合はunique_ptrかshared_ptrで返すよ

うにする** 汎用性を考えるならunique_ptrで返すべきだろう。shared_ptrで受け取れば暗黙に変換され

る(EMC++項目18参照)。

例外事項ムーブコストが高い型の場合(例えば

array<BigPOD>)、フリーストアで確保してハンドル(unique_ptr等)を返すか、出力専用引数の使用を検討

する。

例外事項ループで何度も関数を呼び出すが、メモリ確保のコストが無視できないためオブジェクトを使いまわしたいような場合には出力専用引数を使用する(「caller-allocated

out」パターン)。

ちなみに

tuple型の返り値にはtieを使うと便利

Sometype iter;Someothertype success;tie(iter, success) = myset.insert("Hello"); // tupleを返す。if(success) do_something_with(iter);

Q. スマートポインタがあるから、もう生のポインタを使うことはないの?

A. 所有権がからまないなら、生のポインタで十分。

F.42 位置(position)を示す場合に限り、T*を返す。生ポインタが役立つ例。

所有権がからまないなら生ポインタでOK。(所有権を扱う場合はunique_ptrなどスマートポインタを使う)Node* find(Node* t, const string& s) // 二分木からsを持つノードを探す{ if (t == nullptr || t->name == s) return t; if (auto p = find(t->left, s)) return p; if (auto p = find(t->right, s)) return p; return nullptr;}

……

Warningこれまでどおりの、シンプルで、分かりやすくて、簡単

なやり方を捨ててでも、あるかどうかも不明確な効率が欲しい人のために

(しかし往々にしてどうしても必要な場合がある(特にライブラリ作成者には))

Advanced

(引用元 https://github.com/isocpp/CppCoreGuidelines/blob/master/param-passing-advanced.png)

F.18 「消費する(consume)」仮引数は、X&&とし、moveする。

型がムーブに対応していれば効率的であるし、かつ、呼び出し元がlvalueを与えようとしても明示的にstd::moveしなければコンパイルエラーとなるため、バグを回避で

きる。

例外事項ムーブ可能でムーブコストの低い、排他的所有権を扱う型(unique_ptrなど)はよりシンプルに値渡しもでき、ほぼ同じ効果を得ることができる(余分なムーブが一回発生するが、そのコストは低い。単純明快さのほうが重

要)。f(unique_ptr<SomeType>&&); // badg(unique_ptr<SomeType>); // good 余分なムーブが発生するが単純で明解

Q. で、Forwarding referenceはいつ使うべきなの?

A. 引数を別の関数に転送したい場合。そして、その場合にのみ。

F.19 「転送する(forward)」仮引数は、TP&&とし、std::forwardの

み行う。単に別の関数に引数を転送したい場合、

また、この場合にのみ、引数のconst性とrvalue性は無視させる必要がある。template <class F, class... Args>inline auto invoke(F f, Args&&... args) { return f(forward<Args>(args)...);}

もうひとつ、返り値で注意するべきこと。

F.45 T&&を返さない破壊される一時オブジェクトへの参照を返してしまう。

一時オブジェクトを関数に渡すのは安全(一時オブジェクトの寿命は関数呼び出しが終わるまで持つ)

一時オブジェクトを関数から返すのは危険(呼び出し元が受け取った時点で、一時オブジェクトの寿命は既に尽

きている)

え?

moveとforwardは?

例外事項moveとforwardはT&&を返すが、キャストしてるだけだからOK。

オブジェクトは寿命が尽きる前に式に渡される。

他にT&&を返す例が思いつかない(by Stroustrup & Sutter)

結局、引数や返り値はこれまでのやり方でだいたいOK

ところで、クラス書いてますか?

クラスにはコンパイラが暗黙に生成しうる

特別なメンバ関数がある。

暗黙に生成しうる関数デフォルトコンストラクタコピーコンストラクタコピー代入演算子ムーブコンストラクタムーブ代入演算子デストラクタ

Q. どれをいつ自分で定義していつコンパイラが勝手に生成する

のを抑制するべき?

A. 全か無か

C.20 コンパイラが暗黙に生成する関数をまったく定義する必要がな

いなら、全て定義しない。もっとも単純で、もっとも明解。

struct Named_map {public: // ... 特別なメンバ関数を定義しない ...private: string name; map<int, int> rep;};

Named_map nm; // 暗黙に生成されたデフォルトコンストラクタが呼び出される。Named_map nm2{nm}; // 暗黙に生成されたコピーコンストラクタが呼び出される。

これと関連して

C.21 コンパイラが暗黙に生成する関数を一つでも定義もしくは

=deleteするなら、他の関数も全て定義もしくは=deleteする。

コンパイラが暗黙に生成する関数はどれも密接に関連しているため、そのうちの一つでもデフォルトの挙動がそぐわないなら、恐らく他の関数も修正する必要がある。

struct M2 { // bad: incomplete set of default operationspublic: // ... // ... no copy or move operations ... ~M2() { delete[] rep; }private: pair<int, int>* rep; // zero-terminated set of pairs};void use() { M2 x; M2 y; // ... x = y; // the default assignment // ...}

暗黙に生成される実装で良い場合は=defaultする。(暗黙に生成される実装と同じものを明示的に定義する

ことになる)

暗黙に生成してほしくない場合は=deleteする。(従来のprivateで宣言する方法よりもコンパイルエラー

が分かりやすくなる。EMC++項目11参照)

基本的に、特別なメンバ関数は全て書く/全て書かないの二択。

Q. ところで、デフォルトコンストラクタって必要なの?

A. はい

C.43 クラスがデフォルトコンストラクタを確実に持つようにする

言語機能と標準ライブラリの中にはオブジェクトがデフォルトコンストラクタで初期化可能であることを前提

にしているものがたくさんある。

vector::vector(size_t)class Date {public: Date(); // ...};

vector<Date> vd1(1000); // デフォルトコンストラクタが必要(*)vector<Date> vd2(1000, Date{Month::october, 7, 1885}); // こうすれば合法ではある

* 厳密にはDefaultInsertableコンセプトを満たせばOKではあるが、アロケータをカスタマイズするのでもない限りデフォルトコンストラクタが必要と考えてよい。

(再掲)tieSometype iter; // デフォルトコンストラクタが呼ばれる。Someothertype success; // 同上。tie(iter, success) = myset.insert("Hello");if(success) do_something_with(iter);

Q. メンバ変数を特定の値に初期化したいだけなのに、そのためにデフォルトコンストラクタを書い

て、そのために他の特別なメンバ関数を定義しないといけなくなる

のって大変じゃない?

A. リスト初期化(listinitialization*)があるから

大丈夫。* initializer_listとは別物

C.45 メンバ変数を初期化するだけのデフォルトコンストラクタを定義しない。代わりに、クラス内メンバ初期化(in-class member

initializers)*を使う。* メンバ変数のリスト初期化を指していると思われる

メンバ変数のリスト初期化の例class Bad { // メンバ変数をリスト初期化するべき string s; int i;public: X1() :s{"default"}, i{1} { } // 他の生成される関数を明示的に定義する必要がある(C.21) // ...};

class Good { // より効率的 string s = "default"; int i = 1;public: // コンパイラが生成したデフォルトコンストラクタを使う // ...};

コンストラクタ書いてますか?

複雑なコンストラクタを書くよりもinit()メンバ関数を別に用意した

ほうがいいの?

A. いいえ。

C.41 コンストラクタは完全に初期化されたオブジェクト

を生成するべき

コンパイラはコメントを読まない恐らくプログラマも読まない

class X1 { FILE* f; // 他の関数を呼ぶ前にinit()をまず呼び出せよ! 絶対だぞ! // ...public: X1() {} // 何もしない可愛らしいデフォルトコンストラクタ。 void init(); // fを初期化する void read(); // fから読み出す。 // ...};void f() { X1 file; file.read(); // クラッシュするか、まったく無意味な値を読み出す。 // ... file.init(); // もう遅い。 // ...}

オブジェクトを生成した後、init()メンバ関数を呼び出す必要があるなら、設計を見直す。

コンストラクタだけではうまく初期化できないなら、ファクトリ関数を使う。

複数のコンストラクタで共通の処理があるなら、デリゲートコンストラクタを使う。

ファクトリ関数の例class B {protected: virtual void PostInitialize() { ... f(); ... } // 仮想関数をディスパッチするpublic: virtual void f() = 0; template<class T> static shared_ptr<T> Create() { // コンストラクタの代わり auto p = make_shared<T>(); p->PostInitialize(); return p; }};class D : public B { ... }; // 派生クラス shared_ptr<D> p = D::Create<D>(); // Dオブジェクトの生成

Q. デリゲートコンストラクタ?

A. あるコンストラクタから他のコンストラクタを呼べる機能

C.51 全てのコンストラクタに共通する動作をさせるために、デリゲートコンストラクタを使う。

同じことを何度も繰り返すのは退屈な上に失敗しやすい。

悪い例class Date { // Bad: 似たコードの繰り返しがある。 int d; Month m; int y;public: Date(int ii, Month mm, year yy) :i{ii}, m{mm} y{yy} { if (!valid(i, m, y)) throw Bad_date{}; }

Date(int ii, Month mm) :i{ii}, m{mm} y{current_year()} { if (!valid(i, m, y)) throw Bad_date{}; } // ...};

良い例class Date2 { int d; Month m; int y;public: Date2(int ii, Month mm, year yy) :i{ii}, m{mm} y{yy} { if (!valid(i, m, y)) throw Bad_date{}; }

// 他のコンストラクタを呼び出す。 Date2(int ii, Month mm) :Date2{ii, mm, current_year()} {} // ...};

Q. コンストラクタで例外を投げるのはだめなの?

A. 大丈夫。

C.42 コンストラクタでオブジェクトの生成に失敗した場合は、例外

を投げる。未完成のオブジェクトを放置するのはトラブルの元。

例外事項飛行機の運転システムのようなハードなリアルタイム処理が要求されるシステムなどではエラー処理の挙動が予

測不可能な場合がある。そのような場合はis_valid()テクニックを使い、一貫して生成したオブジェクトをチェックするようにする。

Q. デストラクタの場合は?

A. 失敗は死を意味する。

C.36 デストラクタは失敗してはならない

デストラクタが失敗する恐れがある場合に、どうすれば安全なプログラムを作成できるかは一般的にはわかって

いない。

標準ライブラリは全ての扱う型がデストラクタでエラーを発生させないことを要求する。

コンストラクタとは異なることに注意(コンストラクタは基本的にエラーを投げたほうがよい)。

C.37 デストラクタをnoexceptにする

デストラクタがエラーを投げてプログラムが続行するくらいならterminateするほうがマシ。

ラムダ使ってますか?

Q. ラムダ便利。関数オブジェクトと比べて何も新しいことができるようになったわけでもないのに。

A. 質問ではない。

ES.28 複雑な初期化を行う場合に、ラムダを使う。特にconst変数を初

期化する場合。ラムダはローカル変数の初期化に利用できる。

特にconst変数にできるのは嬉しい。

ラムダでローカル変数の初期化// Badwidget x; // constであるべき。しかし……for(auto i=2; i <= N; ++i) { // 後で初期化するため、constにできない。 x += some_obj.do_something_with(i);}

// Goodconst widget x = [&]{ // constにできる! widget val; // デフォルトコンストラクタで初期化 for(auto i=2; i <= N; ++i) { val += some_obj.do_something_with(i); } return val;}();

取るに足らないけれど、悩ましい問題。

Q. コード書く時にCamelCaseにしたほうがいいの? それとも

snake_caseのほうがいいの?

A. 選択肢の有無で判断する。

NL.10 CamelCaseの使用は避ける迷う。「camelCaseのほうがよくない?」

標準ライブラリは基本的にsnake_caseで書かれている。

デフォルトでsnake_caseにする。

ただし、既にsnake_case以外で書かれたコードがある場合は、そのスタイルに従う。

個人の趣向よりもコードの一貫性が大事

C++ Core GuidelinesまとめF.15 なるべくこれまでどおりのシンプルなやり方で情報

を渡す

F.7 一般的な用途では、スマートポインタではなくT*で引数を受け取る

R.30 ライフタイムに関連する操作を行う場合にのみ、スマートポインタを仮引数とする

F.20 出力用の仮引数を用意するよりも、なるべくreturnで返す。

F.21 複数の値を出力したい場合はなるべくtupleやstructを使う。

C++ Core GuidelinesまとめF.42 位置(position)を示す場合に限り、T*を返す。

F.18 「消費する(consume)」仮引数は、X&&とし、moveする。

F.19 「転送する(forward)」仮引数は、TP&&とし、std::forwardのみ行う。

F.45 T&&を返さない

C++ Core GuidelinesまとめC.20 コンパイラが暗黙に生成する関数をまったく定義す

る必要がないなら、全て定義しない。

C.21 コンパイラが暗黙に生成する関数を一つでも定義もしくは=deleteするなら、他の関数も全て定義もしくは

=deleteする。

C.43 クラスがデフォルトコンストラクタを確実に持つようにする

C.45 メンバ変数を初期化するだけのデフォルトコンストラクタを定義しない。代わりに、クラス内メンバ初期化

(in-class member initializers)を使う。

C++ Core GuidelinesまとめC.41 コンストラクタは

完全に初期化されたオブジェクトを生成するべき

C.51 全てのコンストラクタに共通する動作をさせるために、デリゲートコンストラクタを使う。

C.42 コンストラクタでオブジェクトの生成に失敗した場合は、例外を投げる。

C.36 デストラクタは失敗してはならない

C.37 デストラクタをnoexceptにする

C++ Core GuidelinesまとめES.28 複雑な初期化を行う場合に、ラムダを使う。特に

const変数を初期化する場合。

NL.10 CamelCaseの使用は避ける

今回話さなかったことGudeline Support Library(GSL)

C++17

他にも山ほどあるルール

全体のまとめ

C++ Core Guidelinesを読もう精読する必要はない。

まずは、ざっと目次を眺めて、気になるものがあれば、そこから見ていく。

精読するとモダンなC++の作法が網羅的に分かる

Effective Modern C++を読もう手に入らない人は公開されているものを読もう

第一章「型推論」http://www.oreilly.co.jp/pub/9784873117362/ch01.pdf

EMC++勉強会@東京まとめhttps://github.com/herumi/emcjp

規格書を辞書として使う精読する必要はない。

知りたい単語で検索をかけて関係ありそうな部分を読んでみる

真の全体のまとめ

C++を学ぶなら英語を読もう

裏の全体のまとめ

Q. なぜ我々はコードを書くのか?

A. 現実は多様性に富んでいるから

大事なのは理由