ホーム > 読んだ > 国内

プログラミングの禁じ手 C++言語編
プログラミングノキンジテ シープラスプラスゲンゴヘン

書誌

text唯野
author真紀俊男
publisherソフトバンク
year『C Magazine 2000.7』p.12-38

目次

1感想
2抄録

履歴

2000.9.1読了
2000.9.3公開
2001.7.15修正

感想

原則的な意味での C++ べからず集。まあ、この辺を理解した上で『Effective C++』を読むというのが常道なのでしょう。

抄録

12-13/19/21/23

C++ は C に対するハイブリッド言語であるが、だからといって学びやすいわけではなく、機能の追加だけでないダークサイドな側面も同じくらい(それ以上 ?)に増えている。C++ の利点といえる大規模プログラムへの応用は、すぐに大きな効果の出るものではないため、始めから長期的な視野を念頭に入れておく必要がある。大規模プログラムでのポイントはプログラムの分かりやすい分割が鍵といえるため、C++ のような大規模なプログラム言語では思考の負担をできるだけ減らすようにした方がよい。例えば、広範囲への影響箇所や外部からのアクセス箇所といった副作用の生みやすい要因を減らすようにしたり、チーム開発において各人が自分に都合よく解釈・作業するといった事態を防ぐということ。或いは、新しいプログラム言語を自分のそれまで経験していた言語の延長線上で捉えないようにすることなどが挙げられる。

ちなみに、メッセージ(オブジェクトへの命令)とメソッド(オブジェクトへの挙動)の関係についてであるが、大抵の OOPL では区別されている両者が、C++ では効率性重視の結果、メッセージとメソッドは通常同じで、そうでない場合に仮想関数を使うというかたちになっている。

14-17 規模や複雑さに関する禁じ手

継承レベルが深い -> プログラムの全体像が捉えにくくなり、バグの原因追求も困難になる。また、派生先から利用できる継承元のメンバ関数を遡って調べるのが大変になる。

多重継承が多いか悪用している -> 多重継承の必要性を理解していないと悪い状況(菱形継承など)を招きやすい。多重継承は所有によって解決できることもあるが、実装継承とインタフェース継承を区別することがポイントとなる。

メンバ関数/メンバ変数が多い -> 変数を探すのが大変となり実装関数に対する検証も多く必要となる。商用ライブラリで陥りがちな問題で、検証による分割などを検討すべきといえる。

サイズの大きなメンバ変数をオート変数として持つ -> スタック領域に制限のある環境(組み込みシステムなど)でハングアップや停止が起こるようになる。

複数のメンバ関数をややこしく使う -> 複数のメンバ関数でひとつの作業を行ったりしようとすると、関数の呼び出し手順や呼び忘れといった問題を引き起こす。これはオブジェクトの場合でも同じで、オブジェクトの生成、消滅、コピーが複雑な依存関係にあると同様の問題を生むことになる。

18-23 隠蔽とスコープに関する禁じ手

インタフェースと内部実装を厳密に区別し、内部実装は隠すようにするということ。

メンバ変数を public にしない -> メンバ変数を直接的に外部から触れられないようにすることで仕様変更に強く、メンバ変数を直接扱う場合に比べ必要となるチェック処理の数を減らすことができる。もちろん、メンバ関数や基底の public メンバでも同じことがいえる。(クラスのメンバ関数の場合にはそれを実装のない private メソッドにするという回避策がある。)

内部実装をさらす(メンバ変数へのポインタを得るメンバ関数)がある -> やむをえない場合でも const として渡すようにすべきである。

複数のインスタンスから共有されたくない一時変数を共有する(static にする) -> 全てのインスタンスで共有すべきものとそうでないものとは明確に区別されなくてはならない。つまり、クラスとインスタンスを混同しないということ。(クラスはひとつだがインスタンスは複数ありえる。)

friend を悪用する -> 書き換えによるバグの原因追及が困難となる。friend は演算子オーバーロードやコンテナのイテレータなど限られた局面でのみ使うようにした方がよい。

使われたくない暗黙のメンバ関数を未定義にしない -> コピーコンストラクタや代入演算子が暗黙のうちに定義され、それがポインタ変数を扱う場合、それらはポインタだけをコピーするので問題となる。ちゃんと関数を用意するか private にして回避しなければならない。

23-30 仕様の誤解に関する禁じ手

デストラクタを virtual にしない -> 発覚しにくいメモリリークや解放し忘れといった問題を引き起こす。

コンストラクタ・デストラクタの起動する順序が曖昧 -> 移植性が低く再現性の悪いバグに見舞われる。外部変数オブジェクトは生成順序が明確でないため、複数のソース間における生成順序は固定されない。そのため、外部変数そのものを減らすか、外部変数をポインタにして明示的に new してコンストラクタを呼び出すようにする必要がある。

new/malloc と delete/free を混同する -> 両者は別物なので、それを知らずに使うと移植性が低くなる。これに関連して配列オブジェクトにおける new/delete の [ ] の付け忘れもありがちといえる。

コピーコンストラクタと代入演算子を混同する -> 両者を混同するとプログラムが期待通りに動かなくなる。個人開発よりもチーム開発において起こりやすい問題のひとつ。

二重に delete する -> これを防ぐために一度解放したものには NULL を代入してしまうというテクニックがある。同様に不定の値で delete を行うケースも含む。

継承先のコンストラクタがどの基底クラスのコンストラクタを呼び出すのか確認しない -> 期待しない動きとなることがあるので、呼び出すべきコンストラクタを明確にしたい場合には、それを明示しなければならない。

違うクラスのメンバをキャストで無理やり使う -> キャストに伴う弊害を理解できておらず、挙動が明らかではない処理となる恐れがある。これは C でも同じこと。

NULL や不定値のリファレンスを作る -> リファレンスをポインタと混同するとリファレンスの初期化忘れや無効値の代入、オブジェクトの加減算とアドレスの加減算の混同――などを引き起こしやすい。これはメンバ変数などでも同様である。

演算子オーバーロードを悪用する -> 必然性のないオーバーロードを定義すると、それに伴うバグが関数と比べても見つかりにくくなる。

既に利用されている基底クラスの仕様を変える -> それを既に利用している部分への影響が大きく他のメンバーへの負担も大きくなる。

誰が確保/解放するのか不明瞭なメンバポインタ変数を持つ -> 不定値のポインタによるメモリリークが起こりやすくなり原因追求も難しくなる。そのためコンストラクタで意味のある値か NULL で初期化し、確保/解放をクラス内部の責任にしてデストラクトするようにする。

new の失敗を考慮しない -> bad_alloc 例外に対する catch ハンドラか、それに対する異常処理を用意するようにする。

31-33 例外に関する禁じ手

例外を使うと正常処理と異常処理を分離することができる利点のある反面、どこから投げられたかの判断が難しい/どこから処理を再開すべきか判断しにくい/不十分な例外後の後始末を招きやすい――といった問題がある。しかし、うまく例外を使うことですっきりした保守性の高いプログラムを作ることができる。

コンストラクタ内で例外を起こす -> コンストラクタで処理が完結しないとデストラクタが呼ばれないためメモリリークを引き起こす。そのためコンストラクタでは例外を起こさないようにするか、例外が起きてもよいような作り(初期化関数を別途用意するなど)にする必要がある。これはデストラクタでも似たような問題が存在する。

ポインタを throw する/「throw new 例外クラス」する -> 確率的に低いが catch したポインタの解放し忘れやメモリリークを引き起こすので、直接例外を投げるか別の場所で確保済の例外オブジェクトのリファレンスを投げるようにした方がよい。

例外クラスの中で例外が起こりうる -> メモリ不足の例外で更にメモリの確保を行うような場合に注意するということ。二重例外が起こらないようなロジックを組むようにする。

全文を読まれる場合はログインしてください


Up