boost.勉強会#19東京 effective modern c++とc++ core guidelines
TRANSCRIPT
C++11/14で関数はどのように書くべきなのか?
クラスはどのように書くべきなのか?
{}による初期化で気をつけるべきことは?
いつForwarding Referenceを使うべきなのか?
......
そこでEffective Modern C++
(引用元 https://www.oreilly.co.jp/books/images/picture_large978-4-87311-736-2.jpeg)
9月に邦訳も出ました「C++11/14プログラムを進化させる42項目」
(引用元 https://www.oreilly.co.jp/books/images/picture_large978-4-87311-736-2.jpeg)
EMC++第一章「型推論」まるまるPDFで公開されている
http://www.oreilly.co.jp/pub/9784873117362/ch01.pdf
EMC++が手に入らない場合EMC++勉強会@東京の資料が公開されている
https://github.com/herumi/emcjp
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++コーディングにおける実証済みのガイドラインとルール、そしてベストプラクティスの集合体である」
C++ Core Guidelinesはまだ未完成アプリケーションやライブラリの設計に対するルール集
であり、C++のチュートリアルではない。
複数のコードベースをまたいでより統一的なC++コードスタイルを確立することを目指す。
基本的に新しくコードを書くことを前提にしている。
内容が直感、経験に反するかもしれない。修正歓迎。
C++ Core Guidelinesは実用性重視なるべくたくさんのルールを入れる。
内容的な重複も厭わない
一見、とるに足らないようなルールもある。(初心者がいることを忘れるな!)
完璧に合法なC++のためにエラーが起きやすいコードを書くようなことは勧めない。
C++のサブセットでJavaを作ったりはしない。
将来的に解析ツールでルールが守られているか自動でチェックでき
るようにするプログラマがルールを全部知っておく必要はないように
したい
解析ツールが参照しやすいようにルールにインデックスを付ける(例:F.15、ES.30)
スマートポインタ(uniqur_ptrとshared_ptr)
unique_ptrは排他的所有権(exclusive ownership)を扱う
shared_ptrは共同所有権(shared ownership)を扱う
Forwarding reference*template<typename TP>void f(TP&& t) { // TP&&がforwarding reference ...}
Forwarding referenceについてはEMC++項目24を参照* EMC++ではユニヴァーサル参照(universal reference)となっている。
「特殊で賢いテクニック」は他のプログラマを困惑させ、コードの理解を妨げ、バグの温床となる。
どうしてもよく知られたやり方以外の方法が相応しいと思うなら、性能が実際に向上することを計測によって確かめ、きちんとドキュメント化しておく(おそらく可搬
性がないため)。
スマートポインタで引数を受け取るようにしてしまうと、関数の利用者に対してスマートポインタを使うこと
を強制してしまう。
std::shared_ptrなどの一部のスマートポインタは実行時コストが大きい。
入出力用引数は出力専用引数と混同されやすいが、返り値は問題ない。
返すオブジェクトがSTLコンテナであっても、暗黙的にmoveされる上に、
明示的にメモリ管理する必要がなくなる。
ただし、個別にはムーブコストが安いオブジェクトもそれがたくさん集まった構造体ではコストが累積して高く付くかもしれないことに注意。(例外事項を参照)
例外事項値型でない場合、例えば派生クラスを基底クラスのポインタで返したい場合はunique_ptrかshared_ptrで返すよ
うにする** 汎用性を考えるならunique_ptrで返すべきだろう。shared_ptrで受け取れば暗黙に変換され
る(EMC++項目18参照)。
tuple型の返り値にはtieを使うと便利
Sometype iter;Someothertype success;tie(iter, success) = myset.insert("Hello"); // tupleを返す。if(success) do_something_with(iter);
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これまでどおりの、シンプルで、分かりやすくて、簡単
なやり方を捨ててでも、あるかどうかも不明確な効率が欲しい人のために
(しかし往々にしてどうしても必要な場合がある(特にライブラリ作成者には))
F.18 「消費する(consume)」仮引数は、X&&とし、moveする。
型がムーブに対応していれば効率的であるし、かつ、呼び出し元がlvalueを与えようとしても明示的にstd::moveしなければコンパイルエラーとなるため、バグを回避で
きる。
例外事項ムーブ可能でムーブコストの低い、排他的所有権を扱う型(unique_ptrなど)はよりシンプルに値渡しもでき、ほぼ同じ効果を得ることができる(余分なムーブが一回発生するが、そのコストは低い。単純明快さのほうが重
要)。f(unique_ptr<SomeType>&&); // badg(unique_ptr<SomeType>); // good 余分なムーブが発生するが単純で明解
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はT&&を返すが、キャストしてるだけだからOK。
オブジェクトは寿命が尽きる前に式に渡される。
他にT&&を返す例が思いつかない(by Stroustrup & Sutter)
C.20 コンパイラが暗黙に生成する関数をまったく定義する必要がな
いなら、全て定義しない。もっとも単純で、もっとも明解。
struct Named_map {public: // ... 特別なメンバ関数を定義しない ...private: string name; map<int, int> rep;};
Named_map nm; // 暗黙に生成されたデフォルトコンストラクタが呼び出される。Named_map nm2{nm}; // 暗黙に生成されたコピーコンストラクタが呼び出される。
コンパイラが暗黙に生成する関数はどれも密接に関連しているため、そのうちの一つでもデフォルトの挙動がそぐわないなら、恐らく他の関数も修正する必要がある。
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参照)
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);
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: // コンパイラが生成したデフォルトコンストラクタを使う // ...};
コンパイラはコメントを読まない恐らくプログラマも読まない
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オブジェクトの生成
悪い例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()} {} // ...};
例外事項飛行機の運転システムのようなハードなリアルタイム処理が要求されるシステムなどではエラー処理の挙動が予
測不可能な場合がある。そのような場合はis_valid()テクニックを使い、一貫して生成したオブジェクトをチェックするようにする。
デストラクタが失敗する恐れがある場合に、どうすれば安全なプログラムを作成できるかは一般的にはわかって
いない。
標準ライブラリは全ての扱う型がデストラクタでエラーを発生させないことを要求する。
コンストラクタとは異なることに注意(コンストラクタは基本的にエラーを投げたほうがよい)。
ラムダでローカル変数の初期化// 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;}();
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にする
Effective Modern C++を読もう手に入らない人は公開されているものを読もう
第一章「型推論」http://www.oreilly.co.jp/pub/9784873117362/ch01.pdf
EMC++勉強会@東京まとめhttps://github.com/herumi/emcjp