『プログラミング言語 C++ 第4版』(ビャーネ・ストラウストラップ著)第3章の学習メモ。
本章で重要と感じた・疑問に思ったポイントの要約と、理解があいまいだった用語の整理を行う。
用語メモ
用語 | ページ数 | 用語の意味 |
---|---|---|
内部データ表現 | 68 | プログラマがソースコードで書いたデータ(int, float, struct, class など)が、コンピュータ内部でどういう形式で表現・格納されているか を指す。 |
具象クラス | 68 | すべての仮想関数を実装済み → インスタンス化できる。 |
抽象クラス | 68 | 少なくとも1つ以上の純粋仮想関数を持つ → インスタンス化できない。インターフェースや基底クラスとして使われる。 |
仮想関数テーブル(vtbl) | 76 | ポリモーフィズム利用の際、正しい関数を実行時に呼び出すための情報を持つテーブル。仮想関数を持つ各クラスが専用のvtblを持ち、仮想関数を特定する。 |
セマンティクス | 81 | プログラムの意味(それに伴う動作) |
右辺値 | 83 | 関数が返す整数などのような、代入できない値のこと。 |
右辺値参照 | 83 | 他の誰も代入を行えない何かを参照するもの。安全に値を盗むことができるもの。C++では「&&」で表現する。 |
std::thread | 84 | プログラムの中で 並行処理(複数の処理を同時進行させる) を行うための仕組み。C++11で導入。 |
パラメータ化 | 85 | 固定的な処理を柔軟にするために、変化する部分を外から渡せるようにすること。 |
実装継承 | 92 | 親クラスの実装を子クラスが再利用する 継承のこと。 |
インターフェース継承 | 92 | 派生クラスに共通のインターフェースだけを与える継承のこと。純粋仮想関数を持つクラスで表現。 |
ポリシー(振舞い) | 93 | 「どういう方針で処理をするか」という戦略やアルゴリズムの選び方。並べ替えで「昇順か降順か」、探索で「深さ優先か幅優先か」など。 |
重要・疑問ポイントまとめ
コンストラクタで資源を獲得して、デストラクタで解放する技法は、資源獲得時初期化=RAIIと呼ばれ、"裸のnew演算"を削減する効果がある。(p72)
デストラクタはvirtualである。これも抽象クラスの共通点だ。(p74)
基底クラスのポインタで派生クラスを指してdeleteするとき、デストラクタが仮想でなければ 派生クラス部分の破棄がされないため。
int main() {
Base* p = new Derived();
delete p; // ← Baseのデストラクタが仮想(virtual)でない場合、派生クラスが破棄されない。
}
この柔軟性の代償は、オブジェクトの操作を、ポインタや参照経由で行わなければならないことだ。(p75)
オブジェクトの操作:オブジェクトが外部に提供する振る舞い(メソッド呼び出し、演算子オーバーロードなど)を通じて、オブジェクトの状態を取得したり変更したりすること。
C++ のオブジェクトは 静的型 であり、通常の変数(値オブジェクト)は「どの型で生成されたか」がコンパイル時に固定される。オブジェクトの「完全な正体」を保持したまま基底型として扱うには、ポインタや参照を使うしかない。
int main() {
Derived d;
Base b = d; // スライス、Derived 部分が失われる
b.hello(); // → "Base"
Base& r = d; // 参照ならスライスしない
r.hello(); // → "Derived"(ポリモーフィズムが働く)
Base* p = &d; // ポインタも同様
p->hello(); // → "Derived"
}
仮想呼び出しの効率性は、"通常の関数呼出し"と比べても遜色ない(その差は25%以内だ)。
メモリ空間のオーバーヘッドは、仮想関数を持つクラスのオブジェクト1個ごとに1個のポインタ、そして、仮想関数を持つクラスごとに1個のvtblである。(p76)
メモリ空間のオーバーヘッドは、vtblとvtblへのポインタ(vptr)のこと。
呼び出されたデストラクタ(※派生クラスのデストラクタ)は、暗黙裡にメンバのデストラクタと基底クラスのデストラクタとを呼び出す。(p78)
クラスを設計する際は、オブジェクトがコピーされる可能性とコピーの方法を必ず検討しなければならない。単純な具象型であれば、メンバ単位のコピーが正しいセマンティクスとなることが多い。Vectorのような高度な具象型では、メンバ単位のコピーは、正しいセマンティクスとはならない。また、抽象型では、メンバ単位のコピーは、まず、ありえない。(p81)
※単純な具象型:座標や色を持つだけの struct みたいなもの。
高度な具象型の場合、メモリの二重解放などが起きる可能性がある。抽象型の場合、そもそも「コピーする」という考え方自体が正しくない。
ムーブ後に、ムーブ元オブジェクトは、デストラクタが実行できる状態へと遷移する。通常、ムーブ元オブジェクトに対する代入は行えるようになっている。(p84)
移動(move)するのは「デストラクタが解放する対象(リソースの所有権)」。
ムーブ元は使えなくなるわけじゃない。ただ中身が「未規定だけど安全」な状態になる(たとえば空文字列やnullptrなど)。
階層内のクラスに対して、デフォルトのコピーやムーブを利用すると、ほとんどの場合は惨事につながる。(p85)
階層内のクラス:基底クラス・派生クラスといったクラス継承の階層に含まれるクラスを指す。つまり、「クラス継承を使っている場合に、デフォルトのコピーコンストラクタやムーブコンストラクタをそのまま使うと、思わぬ問題(リソースの二重解放など)につながる可能性がある」ということ。対策としては、デフォルトのコピー演算・ムーブ演算を削除(=delete)すること。
ユーザが明示的にデストラクタを宣言したクラスに対しては、ムーブ演算が暗黙裡には生成されない。
~
たとえコンパイラがデストラクタを暗黙裡に生成するような場合であっても、デストラクタを明示的に定義する理由の一つである。(p85)
デストラクタをユーザが書くということは、「オブジェクト破棄時に特別な処理が必要」という意思表示であり、
その場合、コンパイラは「単純にメンバをムーブすればいい」という安全な仮定ができなくなるから。
テンプレートは、コンパイル時のメカニズムなので、手作りのコードに比べて、実行時のオーバーヘッドが増えることはない。(p87)
実行時のオーバーヘッド:言語やライブラリの仕組みを利用する見返りとして余分にCPU時間やメモリを使う処理。
型やテンプレートに同義語を与えるのは、有用なことだ。(p90)
可搬性の高いコードを記述できるため。
// プラットフォームごとに定義可能
using Index = long; // ある環境では 32bit
// 別の環境では
using Index = long long; // 64bit が必要
ストラウストラップ先生からのアドバイス
( "→" 以降は補足説明。)
-
アイディアは、そのままコード化しよう。
→ 無理に抽象化や最適化を先取りせず、まずはシンプルに表現することが大切。 -
アプリケーションのコンセプトを、そのままコード化したクラスを定義しよう。
→ コードはドメインの概念を反映すべき。クラス名や設計がそのまま問題領域を映すようにすると理解しやすい。 -
単純なコンセプトの表現と厳しい性能とが求められる部品には、具象クラスを利用しよう。
→ 抽象クラスを使うとわずかなオーバーヘッドが生じる。
→ 「単純なコンセプト」とは、std::string
やstd::vector
のように実装が1種類で十分なものを指す。 -
裸のnew演算子とdelete演算子は使わないよう。
→ 手動でのメモリ管理はバグやリークの温床になる。std::make_unique
やstd::make_shared
などの資源管理ツールを使うべき。 -
資源管理には、資源ハンドラとRAIIを利用しよう。
→ コンストラクタで資源を獲得し、デストラクタで自動解放するのが基本方針。例:std::unique_ptr
やstd::lock_guard
。 -
インターフェースと実装の完全な分離が必要であれば、インタフェースとして抽象クラスを利用しよう。
→ 複数の実装を切り替えたり、ライブラリの利用者に実装を隠蔽したいときに有効。典型例はstd::istream
/std::ostream
。 -
階層構造をもつ概念を表現するには、クラス階層を利用しよう。
→ 「動物 → 哺乳類 → 犬」のように自然な階層がある場合は、継承を使うと直感的に設計できる。 -
クラス階層を設計する際は、実装継承とインターフェース継承を使い分けよう。
→ コード再利用が目的なら実装継承、ポリモーフィズムが目的ならインターフェース継承。両者を混同すると設計が崩れる。 -
オブジェクトの構築、コピー、ムーブ、解体を制御しよう。
→ デフォルトに任せるべきときと、自分で定義して制御すべきときを区別する。例:コピー禁止にするならコンストラクタをdelete
する。 -
コンテナは値で返却しよう(ムーブを活用できるので効率的だ)。
→ C++11 以降はムーブセマンティクスにより、値返却でもコストは低い。std::vector<int> makeVector() { std::vector<int> v; v.push_back(1); v.push_back(2); return v; // ムーブで返される } auto result = makeVector(); // 高速
→ C++03ではコピーが走っていたが、今は「シンプルかつ効率的」に値返却できる。