『Effective C++』は中級者への足がかりとなる濃密なガイドライン
より良い C++ コードを書くためのガイドラインとなる『Effective C++』第 3 版を読みました. (出版社のページ). 有用な知識やアドバイスが多く非常に良い勉強になりました. 学んだことや感想を記します.
感想
C++ の本として有名な本書の名前は聞いたことがあり昔軽く手にとってみたことがあったのですが, いまいちピンとこず, 大事なことが書いてあるのだろうけどよくわからない難しい本という印象をもっていました.
しかし改めて読んでみると, C++ の経験をある程度積んだおかげか, 非常に有用で興味深い内容に感じられました. すでに知っていることが 3 割, 知らなかった or よく理解していなかったことが 7 割程度でした.
今までこんなにも多くのことを知らずに C++ を使っていたのかと愕然としたり, こんなに細かいことまで気にしなければならないのかと引いたり驚いたりしました.
印象に残ったこと
印象に残った項目をいくつかピックアップして振り返ります.
7 項, 36 項
- ポリモーフィズムのための基底クラスには仮想デストラクタを宣言する
- 非仮想関数を派生クラスで再定義するのは NG
まず本書序盤の 7 項で「ポリモーフィズムのための基底クラスには仮想デストラクタを宣言しよう」というタイトルが付けられているのですが, 最初はタイトルを見ても何のことだかさっぱり分かりませんでした. 基底クラスのデストラクタを仮想にしておかないとメモリが正常に開放されないことがあるという内容で, へぇーと思ったのですが, ちゃんと理解できたのは後半の 36 項を読んでからでした.
36 項では, 非仮想関数を派生クラスで再定義すると, 呼び出し方によって基底/派生クラスのどちらの関数が呼ばれるか変わってしまうということが説明されています. これを読んでようやく 7 項の意味が分かりました. 派生クラスは必ずデストラクタを再定義するので, 基底クラスのデストラクタは仮想にしなければならないということです.
この項だけでも継承, 仮想関数, コンパイラによるコンストラクタ/デストラクタの自動生成といったことへの知識が必要になり, 自分の知識のなさや理解の浅さを実感しました. そして, もしこれを知らないままポリモーフィズムを使っていた場合, 原因不明のメモリ異常に悩まされることになっていただろうと思うと恐ろしくもありました. C++ の奥深さに気付かされた印象的な項目でした.
13 項
- リソース管理にはオブジェクトを使う
オブジェクトがスコープを抜けるときデストラクタが自動実行されることを利用してリソースの解法忘れを防ぐという内容です. 本項のようなコードを以前見たことがあり, こんなクレバーな方法があったのかと感動したのでよく覚えています.
「リソースを解放するコードを直接書かなければならないなら, (中略) 何かが間違っている」(p.64) という文は肝に銘じておきたいです. 例えばファイル入力の ifstream を使うなら, close() を決して忘れないように注意するのではなく次のようなクラスを定義して使うということです. デストラクタで自動的に close() が呼ばれるので, 解放漏れやコードが煩雑になるのを防げます.
class AutoCloseIfstream {
public:
AutoCloseIfstream(ifstream& ifs) : _ifs(ifs) {}
~AutoCloseIfstream() { if (_ifs) _ifs.close(); }
private:
AutoCloseIfstream(const AutoCloseIfstream&); // コピー禁止.
AutoCloseIfstream& operator=(const AutoCloseIfstream&); // 代入禁止.
ifstream& _ifs;
};
int main() {
{ // このブロックを抜けるとcloseされる.
ifstream ifs("example.txt");
const AutoCloseIfstream autoCloseIfs(ifs);
string line;
while (getline(ifs, line)) cout << line << endl;
}
return 0;
}
他の例として, スマートポインタはポインタのリソース管理オブジェクトです.
このテクニックはリソース管理だけでなく, 終了時に必ず実行したい処理 (例えば一時ファイルの削除, ログ出力など) に応用できます.
23 項
- 可能な限りメンバ関数でも friend でもない関数を使う
あるクラスを使う関数を作る時, クラスに関連のある関数であってもメンバや frined にするのではなく, 普通の関数にする方が良いという内容です.
個人的にメンバ関数とメンバでない関数(無名名前空間の関数など)の使い分けについて悩んだ経験があったのでしっくり来ました. 関連のあるものをまとめるよりもカプセル化を優先するという方針で, これには賛成できます. メンバ変数はクラスにおけるグローバル変数のようなものなので, メンバ変数にアクセスできる関数が少ない方がクラスの状態をシンプルに保ちやすくなると考えています.
33 項
- 継承した名前を隠蔽しないようにする
基底クラスと同名の関数や変数を派生クラスで定義すると, 基底クラスのものが隠蔽されるという内容です. 隠蔽される対象は名前によって決まり, 引数違いでオーバロードされた関数も全て隠蔽されます.
名前検索のルール (例えば派生クラスで関数を呼び出すと, 関数名は次の順番で検索される: ローカルスコープ -> 派生クラス -> 基底クラス -> 派生クラスの名前空間 -> グローバルスコープ) をよく知らなかったので勉強になったのですが, それ以上に名前が同じなら引数が異なっている関数も隠蔽されるという事実に驚きました. 気付かぬうちに継承されていたオーバロード関数が誤って使われるのを防ぐという理由を聞けば納得の仕様ではありますが, このあたりのことを知らずにコンパイルエラーになれば C++ 意味わからんとなりそうです.
34 項
- 継承したいものによって純粋仮想, 仮想, 非仮想関数を使い分ける
基底クラスの関数は, インターフェースを継承するなら純粋仮想関数, デフォルトの実装を継承するなら仮想関数, 変更不可の実装を継承するなら非仮想関数にするという内容です. 継承のことをインターフェースの継承/実装の継承と分けて考えていなかったのでハッとしました.
デフォルトの実装を仮想関数によって与えると, 本当は派生クラスで実装しなければならない関数でデフォルトのものを誤って使用してしまうことを防ぐため, 純粋仮想関数の実装を書くというテクニックが紹介されています. 知らない人が純粋仮想関数に実装が書かれてるのを見ると「?」となりそうではありますが, まさに Effective なテクニックだと感じました.
結び
他にも感心した点は多くあるのですが, 全て書くと長くなるので一部に絞りました.
冒頭にも書きましたが, 知らないことが多く読んで良かったと思います. 悩んでいたことを解消できるテクニック, 考えもしなかったことを考える契機を与える注意事項, 得心の行くアドバイスなど, 知っておいてよかったと思えるような内容がふんだんに含まれています.
他人の書いた C++ コードの意味が分からない, 問題があってなにか良い方法がないか悩んでいるといった方にはおすすめの本です.
本書の内容をきちんと理解して使いこなすことができれば C++ 中級者といっても良いのではないかと思いました. 今後も定期的に読み返そうと思います.