自分にとっての要点メモ。
第1章:C++に慣れよう
2
- #defineを使うな。定数定義にはconstやenumを、マクロの代わりに(必要であればテンプレート化された)インライン関数を使え。
3
- constは可能ならいつでもつけろ。
- 値渡しで出力を返す関数の戻り値をconstにすることで、関数呼び出しを代入式の左辺に置けなくなる。
func()=10;
とかやりたくないならfunc()
の戻り値をconstにしておくとよい。 - constなメンバ関数はオブジェクトの「ビットレベルの不変性」を確保するが、ポインタ参照先のオブジェクトの書き換えなど「論理的不変性」はコンパイラでは保証してくれない。規約(convention)として「論理的不変性」を確保するべき。
- constと非constなメンバ関数で本質的に同じ実装が必要な場合、const_castを使って非constな実装の中からconstな実装を呼び出してコードの重複を避けることができる。
4
- コンストラクタ内での代入より初期化子リストを使え。
- 異なる翻訳単位にある「ローカルでない静的オブジェクト」(名前空間内で定義されたものを含むグローバルオブジェクト、クラス内やファイル内でstatic宣言されたオブジェクト)の初期化順は不定。「関数スコープ内のstaticオブジェクト」は関数呼び出し時に初期化されるので順序の保証ができる。「staticオブジェクトへの参照を戻すだけの関数」を使うことで未初期化オブジェクトを使ってしまうことを避けられる。
第2章:コンストラクタ、デストラクタ、コピー代入演算子
5
- クラスのデフォルトコンストラクタ、コピーコンストラクタ、コピー代入演算子、デストラクタをユーザが書いていないときは、必要になるとコンパイラが自動生成する。
6
- コンパイラが生成する上記関数はすべてpublicなので、対応するメンバ関数を「private宣言し定義を書かない」ことでオブジェクトの生成やコピーなどの操作を禁止できる。
7
- ポリモーフィズムのための基底クラスには仮想デストラクタを宣言する。それ以外の場合は無駄にvirtualにしない。(仮想関数を持つクラスは通常vptr(仮想テーブルポインタ)を インスタンスごとに 持つのでオブジェクトサイズが大きくなる)
8
- デストラクタから例外を投げるな。デストラクタが呼んだ関数から例外が発生してしまう場合はプログラムの実行をそこで中止するか、自分でcatchして外に漏らすな。
- クラスのクライアントが例外に対処する必要があるならデストラクタでないterminate用の関数を設けよ。
9
- コンストラクタやデストラクタ内では仮想関数を呼び出すな。基底クラスのコンストラクタ内ではまだ派生クラスのインスタンスは作られていないので、現在生成・破棄中のオブジェクトの型における該当関数が実行される。直感に反する動作で混乱を招く。
10
- 代入演算子は *this への参照を返せ。
11
- operator=の実装では自己代入を考慮せよ。書き方によっては自己代入時にデータを破棄してからコピーしてしまう等のリスクがある。
12
- コピー関数はオブジェクトのデータメンバと基底クラス部分のすべてをコピーするように書かねばならない。あとでデータメンバを追加したときにエンバグしないよう注意!
- 実装の重複を避けるためにコピー代入演算子とコピーコンストラクタの一方から他方を呼んだりするな。コンストラクタは新しいオブジェクトを初期化するもので、コピー代入演算子は初期化済みのオブジェクトに対して働くものだ。共通部分は第三のprivate関数に括り出せ。
第3章:リソース管理にオブジェクトを使おう
13
- std::shared_ptr (@C++11) はがんばって使おう
14
- リソース管理オブジェクトのコピーは問題。コピー禁止か、参照を数えるなど対応が必要。
15
- リソース管理オブジェクトに包まれた生のリソースへのアクセス手段は必要。get関数を設けたり暗黙型変換を定義したりするが、一長一短。
16
- newとdelete、new[]とdelete[]は対応付けて使え。そのために配列のtypedefは避けよ。
17
- 「newでオブジェクトを生成し、スマートポインタに渡す」ところまでを独立したステートメントで行え。続きの処理まで1ステートメントで行うと例外発生時にリソースリークが起こりうる。
第4章:デザインと宣言
18
- よいインタフェースとはコンパイル時に間違いを見つけてくれるものである。
- クライアントに制限を覚えさせるより、インタフェースで制限を強制せよ。日付構造体を作って値の制限をかけたり、ファクトリ関数からスマートポインタを返したり。(std::shared_ptrにはデリータを登録できる)
19
- クラスを作るときには以下をチェック。組込み型のような新しい型を作る気でやれ。
- オブジェクトはどのように生成・破棄されるか (コンストラクタ、デストラクタ、メモリの確保・解放)
- オブジェクトの初期化と代入は異なる操作になるか (コンストラクタとコピー代入演算子)
- オブジェクトの値渡しはどのようになるか (コピーコンストラクタ)
- 新しい型のオブジェクトが持てる有効な値の組みあわせは (コンストラクタ・コピー代入演算子・セッタでの不変式によるエラーチェック)
- 新しい型は継承の階層にあてはめられるか (基底クラスの関数、特にデストラクタを仮想にするか)
- 新しい型はどのような型変換を受けるか (型変換関数やexplicitでないコンストラクタで暗黙型変換をするか、明示的型変換をするか)
- 新しい型で意味のある演算子や関数は何か。場合によってはメンバ関数でない関数を作る場合もある。
- コンストラクタ等を必要に応じてprivate宣言する
- アクセス制御
- テンプレート化すべきか
20
- 値渡しよりconst参照渡しのほうが性能が良い場合がある
- 引数の値渡しでスライシングが行われ(基底クラス部分オブジェクトをコピーした基底クラスインスタンスが新規生成され)、仮想関数呼び出しが意図と違う結果になるケースに注意。
21
- 新しいオブジェクトが生成されないといけないときに参照を戻してはいけない。ローカル変数への参照を戻り値にしたり、newしたオブジェクトへの参照を戻り値にしたり、不気味なことをしないように。RVO(Return Value Optimization)が行われる事は最近のコンパイラでは当てにしていいよ。
22
- データメンバはprivate宣言せよ。カプセル化をしっかりしておくことで後で実装を改善するときにクライアントコードを書き換えずに済む。protected宣言しても派生クラスのコードには実装が見えてしまっているので、privateだけがカプセル化になっていると心得よ。
23
- 処理をまとめるだけのためにメンバ関数を増やす等してカプセル化を弱めるな。名前空間でまとめてメンバ関数でない関数を定義する手もある。(いまいちぴんとこない)
24
- 2 + (独自型オブジェクト)のような、暗黙の型変換を利用した演算をやりたいときは非メンバ関数を定義せよ。こういう関数を不要にfriendにするな。
25
- 例外を投げないswapを作る話。細かい話に見えるのでパス。
第5章:実装
26
- 変数の定義は可能な限り先延ばしにせよ。使う直前、かつ初期化する値が確定するまで。
27
- キャストは最小限に。
- C++ではキャストはCほど単純にはとらえられない。暗黙の変換も含め、型変換によってポインタが指すアドレスが変わるケースもある(多重継承ではほぼいつも。単一継承でも起こりうる)。キャストしたときにポインタが変わらないという仮定のもとにコードを書くと、処理系によってはうっかり動くこともあるが可搬性がなくなる。
- dynamic_castは一般に遅い。実行時にクラス名の文字列比較をする実装であること多いため。
28
* オブジェクト内部のデータ(もちろんprivateだよね?)へのハンドル(参照、ポインタ、イテレータ)を戻す関数は可能な限り避けよ。戻り値をconstにすることで外部からカプセル化したはずのデータを外から変更されることは避けられるが、stale handleの問題は依然起こりうる。
29 (超重要)
- 関数内での例外発生によるオブジェクトの不変式の破壊やリソースリークを避け、全てのコードを 例外安全 にせよ。
- 例外安全に3種類ある。より弱い保証しかしない関数を組み合わせて強い保証をする関数を作るのは通常とても困難。
- 基本保証(basic guarantee): 例外発生時もオブジェクトの不変式が保たれる。
- 強い保証(strong guarantee): 関数呼び出しが完全に成功するか、失敗して状態が呼び出し前の状態に戻るかいずれか。
- 投げない保証(nothrow guarantee): 絶対に例外を投げない。ただし、
throw()
宣言すれば良いわけではなく、実装で保証せねばならない。
- 強い保証は「コピーと交換」イディオムで実装できることが多いが、現実的でないこともある。例)データベースへの書き込みのロールバック
30
- ライブラリの「クライアントが利用できるインライン関数」の動作修正はバイナリパッチでは困難なことに注意。
- 関数テンプレートの実装がヘッダにあるからといって、何も考えずにinline化するな。
31
- ファイル間のコンパイル依存性を小さくする
- 参照やポインタで十分なときはオブジェクトの使用を避ける。参照やポインタは型の宣言だけで使えるが、オブジェクトの生成には型の定義が必要。
- クラスの定義でなく宣言に依存するコードを書く。
- 宣言と定義でヘッダファイルを分ける。
- 宣言と定義の分離のためにハンドルクラス、インタフェースクラスという技法がある。
第6章:継承とオブジェクト指向設計
32
- public継承はis-a関係を意味するが、public継承の意味するis-a関係はときに直感に反する。ルールは、「基底クラスに適用できるものは全て派生クラスに適用できるようにしなければならない」。
33
- 継承における名前の隠蔽:型は関係なく(変数であれ異なるシグネチャの関数であれ)、派生クラスで宣言された名前は基底クラスの同じ名前を隠蔽する。
- 基底クラスで関数がオーバーロードされていても、派生クラスで同名の関数や変数が1つでも宣言されると全ての引数型の関数が隠蔽されてしまう。隠蔽された名前はusing宣言や「仕事を送る関数」で可視化できる。
34
- インタフェースの継承と実装の継承は異なる。
- 純粋仮想関数はインタフェースのみの継承。
- 純粋でない仮想関数はインタフェースと「デフォルト実装」の継承を意味する。
- デフォルト実装を暗黙のうちに引き継がせたくないときは、基底クラスで純粋仮想関数の実装を記述しておき、派生クラスから
BaseClass::method()
等として純粋仮想関数の実装を明示的に呼び出すとよい。 - 非仮想関数はインタフェースと「変えてはならない実装」の継承を意味する。非仮想関数はオーバライドするな。
35
仮想関数の代わりになるもの。
- NVI(Non-virtual interface)イディオム:仮想関数はprivateにしろ主義に基づき、publicな非仮想関数からprivateな仮想関数を呼び出す。前処理・後処理はpublic非仮想関数の側に書いておくことで、派生クラスで上書きされることを避ける。
- ストラテジパターン: コンストラクタに関数ポインタまたはstd::functionオブジェクトを渡して呼ばせる。
36
- public継承のときに非仮想関数を派生クラスで再定義するな。同名の非仮想関数の挙動が異なるとis-a関係とはいえなくなる。これは#7のデストラクタの話の一般化に相当する。
37
- 継承された仮想関数のデフォルト引数値を変更しないこと(非仮想関数はそもそも継承しないこと)。デフォルト引数の解決は静的に行われるため、呼び出される関数は動的に決まるのに引数は静的に決まるという混乱を招く挙動となる。
38
- コンポジション(オブジェクトが別のオブジェクトを内部に持つ)は、オブジェクトがアプリケーションドメインにある(オブジェクトが実世界の何かに対応する)場合has-a関係、オブジェクトがインプリメンテーションドメインにある(実装上必要になるだけのオブジェクトである)場合is-implemented-in-terms-of関係を意味する。
39
- private継承はis-implemented-in-terms-of関係を意味する。たいていの場合コンポジションに劣る。例外は、派生クラスが基底クラスのprotectedメンバにアクセスする必要がある場合や仮想関数をオーバライドする必要がある場合。(そういう必要性が生じるような実装を避けていればprivate継承はいらないと読める。じゃあいつインプリメンテーションドメインのクラスを設計するのにprotectedメンバを作ったり仮想関数をオーバライドさせたりするべきなのか?)
- private継承ではコンポジションと異なりデータメンバがない場合オブジェクトサイズが0になることを避けるEBO(Empty Base Optimization)が働くことがある。
40
- virtual継承にはコストがかかる(厳密にはコンパイラの実装に依存する)
- 多重継承はややこしいので極力避けたほうがいいが、使うことが自然な場合もある。インタフェースクラスからのpublic継承をしつつ実装を助けるクラスからのprivate継承をする場合など。
第7章:テンプレートとジェネリックプログラミング
41
- テンプレートでは暗黙的なインタフェースとコンパイル時ポリモーフィズムが重要。暗黙的インタフェースというのは、テンプレート内の処理コードがコンパイルできるための条件として課せられる、テンプレートパラメータへの条件。 (特定の演算子や関数が定義されているなど)
42
-
template<class T> class Hoge;
とtemplate<typename T> class Hoge;
は同じ。 - ネストされた依存名(テンプレートパラメータCの中で定義された名前)が型かどうかはCの実装を見ないとわからないので、下記のように
C::const_iterator
が型だということをtypename
キーワードで明示する。
ちょっと長いが自作の例。
typename.cpp
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
template <class C>
void printFirst(const C& container) {
typename C::const_iterator iter = container.begin();
std::cout << *iter << std::endl;
}
int main() {
std::vector<int> vi = {0, 1, 2};
printFirst(vi);
std::deque<std::string> ds = {"foo", "bar", "baz"};
printFirst(ds);
}
- ただし、派生クラスを定義するときの基底クラスの指定箇所と、初期化リストで基底クラスを指定する箇所ではtypenameを使わない。(例外)
template<typename T>
class Derived : Base<T>::Nested { // no typename
Derived(int x) : Base<T>::Nested(x) {}; // no typename
}
43
- テンプレート化された基底クラス内の名前へ派生クラスからアクセスする方法: (1) this->をつける、 (2) using宣言する、 (3) 直接基底クラスを示す修飾をする。
下記、自作サンプル。
inherited_template.cpp
#include <iostream>
template <typename C>
class Base {
protected:
C i;
Base(C i):i(i){};
};
template <typename C>
class Derived : Base<C> {
public:
Derived(C i):Base<C>(i){};
using Base<C>::i; // fix 2
void func() {
std::cout << this->i << std::endl; // fix 1
std::cout << i << std::endl; // OK with fix 2, error without fix 2
std::cout << Base<C>::i << std::endl; // fix 3: 仮想関数の動的結合ができないことに注意!
}
};
int main() {
Derived<std::string> d("hoge");
d.func();
}
44
- テンプレートにより実行ファイルサイズが膨れ上がってしまうことがある。一部はコンパイラがなんとかしろよと思った。気になったときにこの項を参照することにしよう。
45
- 互換な型のすべてを受け入れる関数を作るためにはメンバ関数テンプレートを使う。
以下、例。
member_function_template.cpp
#include <iostream>
class Base {};
class Derived: public Base {};
template <typename C>
class MyContainer {
C *pData;
public:
MyContainer(C *pData) : pData(pData){};
template<typename K>
MyContainer(const MyContainer<K>& other) : pData(other.get()) {};
C* get() const {return pData;};
};
int main() {
MyContainer<Derived> pd = new Derived;
MyContainer<Base> pb = pd;
}
46
- #24 のテンプレート版
- 暗黙の型変換はテンプレートの引数の型を調べるためには行われない。
- クラステンプレートに、 *this を含む引数のいずれかについて暗黙の型変換をサポートする関数が必要なら、クラステンプレートの中でその関数をfriend宣言する。
- inline 化が嫌なほど関数の実装が複雑なら、外部のヘルパーテンプレートを呼び出す部分だけをfriend関数内に書くと良い。
例:
friend_func.cpp
#include <iostream>
template <typename T>
class Rational {
T nominator, denominator;
public:
Rational(T nominator, T denominator = 1) : nominator(nominator), denominator(denominator) {};
friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.nominator * rhs.nominator, lhs.denominator * rhs.denominator);
}
void print() {std::cout << nominator << "/" << denominator << std::endl;}
};
int main() {
auto r = Rational<long>(1, 3);
r = 3 * r;
r.print(); // 3/3
r = r * 4;
r.print(); // 12/3
}
47, 48
- traits, TMP (template meta programming)
- いまのところ理解不能。コンパイラむけスクリプト言語みたいなものか??
tmp.cpp
#include <iostream>
template<unsigned n>
struct Fibonacci {
enum {value = Fibonacci<n-1>::value + Fibonacci<n-2>::value};
};
template<>
struct Fibonacci<0>{enum {value = 0};};
template<>
struct Fibonacci<1>{enum {value = 1};};
int main() {
std::cout << Fibonacci<20>::value << std::endl;
}
第8章:newとdeleteのカスタマイズ
49
- std::set_new_handler() にてメモリ確保失敗時のエラー処理関数をカスタマイズできる。
- クラスごとにカスタマイズされたnew_handlerを設定することもできるようだが、マルチスレッド時に問題が出そうな気がする
50
- new/deleteを自分で定義すべきとき
- デバッグ用
- 効率改善用 (オブジェクトの確保・解放頻度やサイズ分布がわかっていれば標準のnew/deleteよりよいものが作れるかも)
- 利用統計・ログ収集
- メモリアライメントの確保 (DSPでのアドレシングとかね)
- 関連のあるデータを近くに置いてキャッシュを効かせる
- どうやるかは必要になったときに読み直せ!
51,52
- new/delete の書き方には規約があるので書くときは読み直せ!
第9章:いろいろな事
53
- コンパイラの警告はよく見ろ。コンパイル・実行できるからといって警告をスルーするな。警告は実行時エラーや誤動作のよいヒント。
54
- 標準ライブラリをよく勉強しろ。
- STL, iostream, wstring, 例外階層, スマートポインタ, std::function, std::bind, ハッシュテーブル, 正規表現, タプル
55
- boost もフォローするといいよ