C++の品質を以下略

2月 9th, 2010

昨日の続き。

私は、自分でも意外なんですが C++ もほとんど使ったことがありません。
でも学生時代 最もよく使っていた PHP よりも、よほどなじみ深い印象があります。なんでだろ。

浅く使っている限りは C 言語と互換性があるので、昨日書いたことはほとんど有効です。
テンプレートとクラスに関する注意事項が増えます。

マクロ編!

  1. ログ周りとインクルードガードを除くすべてのプリプロセッサ マクロを禁止する
    C言語と違い、過去にマクロで実現 していた機能の大半は、もっと安全な方法で実現可能です。
    どうしてもマクロを使う場合は、使う直前で定義し、使ったら即座に #undef してください。
    この禁止は、新たなマクロ定義の話で、MFC等で定義されているマクロを使うなという意味ではありません。

変数編!

  1. global 変数を禁止する
    昨日と同じです。昨日、禁止の理由を1つ書き忘れていました。
    global 変数を使うと、データの流れが追えなくなるため、バグの温床となり、さらに保守改善が劇的に難しくなります。
    関数の入力が引数だけであれば、データの流れを追うのは簡単です。呼び出している場所を見れば、処理を追うための情報がそろっています。
    しかし global 変数を使うと、関数の呼び出し元と、直前に global 変数を変更した場所を探す必要があります。global 変数はどこからでもアクセスできるので、それは非常に労力が大きい作業です。タイミングの問題によって不定である可能性も高いです。つまり、global 変数を使う関数とは、実行タイミングによって動作が不安定です。
    しかも、昨日書いたようにシンボル衝突でビルドできなくなったり、勝手に書き換えられてモジュールが破壊されたりする危険があります。
    デメリットが大きすぎて、多少のメリットがあったって使うことはできないのです。
  2. クラススコープ以外では static 修飾子を禁止する
    グローバルスコープでは、static 修飾子は global 変数のスコープをモジュール内に限定するために使用します。global 変数は禁止なので、これも禁止です。
    関数スコープでは、static 修飾子は変数の生存期間を変更するために使用します。関数の振る舞いが実行する度に変わってしまう、または同時実行時に不確定な挙動をする危険があるので、禁止です。
    クラススコープでは、static 修飾子はクラスフィールド、クラスメソッドを定義するために利用します。これらはなるべくなら使わない方が良いです。クラスと関係する定数、またはフラグ等の単純なデータ、または singleton や mono-state 等のデザインパターンを利用するために使用できます。定数を除き、アクセスレベルは必ず private: にします。
  3. ポインタ変数を禁止する
    決して見間違いではありません。ポインタ変数は禁止です。明示的な delete も禁止です。
    メモリリークを防ぐために、すべてのポインタ変数は RAII パターンによってオブジェクト内に隠ぺいされるべきです。
    標準の汎用的なスマートポインタ (std::scoped_ptr, std::shared_ptr, 等) を使いましょう。
    双方向関連は、従属オブジェクト → 主オブジェクト の方向を弱参照で持ちましょう。 それだけで、ほとんどの循環参照は解消されるはずです。
    また、FILE* や HANDLE もポインタ変数であることに注意してください。標準の汎用的なスマートポインタは、リソース開放用の関数を指定できます (未指定の場合は delete です)。 対応するリソース解放用の関数を渡して、スマートポインタを利用してください。
  4. auto キーワードは、型が自明な場合に利用してもよい
    C++言語の auto は、型推定のキーワードです。 C言語とは役割も使い方もまったく異なるので注意。
    型推定はコード量を減らせますが、保守性が悪くなります。後々のことを考えるなら、使わない方が無難です。
    ただし、型が自明の場合や、テンプレートメタプログラミングを利用していて、型名がとんでもなく長い場合など、可読性が向上する例もあります。
  5. 可能な限り、すべての変数 (仮引数含む) を const で修飾する
    昨日と同じです。
    ただし、const メンバ関数の存在により、C言語よりも遥かに重要なので改めて記述します。
    可能な限り、すべての変数 (特にポインタ型や参照型の仮引数) を const で修飾してください。
  6. 可能な限り、小さいスコープで変数を宣言する
    昨日書き忘れました。C言語も共通です。
    変数は必要な時に宣言し、必ず同時に初期化を行いましょう。
    変数の生存期間は、極力短くするべきです。
    例外はループ文。ループブロックでクラスを初期化すると、コンストラクタとデストラクタがたくさん呼ばれます。これを防ぐために、ループ文の外で初期化をします。ただし、for文の初期化子を使うなりして、極力 スコープを小さくしましょう。

関数編!

  1. 可能な限り、すべての仮引数を const で修飾する
    念のため、ダメ押ししておきます。
    特に、ポインタ変数、参照変数の場合に重要です。これらの変数は、関数の呼び出し元の値を変えてしまうことができます。const で修飾して、「これはポインタ変数/参照変数だけど、関数の呼び出し元の値を変えないよ」と、関数の利用者に示してください。
    大抵のプログラマは、 仮引数が const ではないポインタ変数/参照変数である場合、それを 出力用の引数であると見なします。
  2. 事前条件/事後条件を設計して明記する
    昨日、書き忘れました。
    事前条件とは、関数を正常に実行するために必要な条件です。 例えば、妥当な引数かテストします。
    事後条件とは、関数の実行後に満たされるべき条件です。例えば、戻り値が null ではないなど。参照・ポインタの仮引数に const をつけるのも、事後条件の表明の1種です。
    コメントやドキュメントに明記するのは当然として、コード中にも表明やテストのための分岐を入れるべきです。絶対に。
    C++では、事後条件は、実行中に例外が発生した場合のことも考慮してください。例外は便利な仕組みですが、例外がスローされることを忘れていると、想定外の関数中断によってオブジェクトの内部状態が破損する可能性があります。
    もう一度言います。事後条件は、実行中に例外が発生した場合のことも考慮してください。

最後はクラス編!
なんだけど、挙げるときりがないですよね・・・
細かく項目を作らず、大項目をいくつか挙げておきます。

  1. クラスの”使われ方”を意識する
    3通りあります。作ろうとしているクラスの”使われ方”によって、実装の作法があります。これは必ず守るようにしましょう。

    1. ひとつは「自動変数として確保されるクラス」です。このタイプは、多態性のない単純なデータ型に適しています。
      絶対に private: なデストラクタが必要です。絶対に、です。
      コピー可能ならばコピーコンストラクタと代入演算子をオーバーロードします。コピー不可ならばコピーコンストラクタと代入演算子を private: でオーバーロードします。絶対にどちらかを定義してください。
      等号と不等号演算子をオーバーロードします。
      可能ならば比較演算子をオーバーロードします。
      それ以外の演算子のオーバーロードは、なるべくすべきではありません。表現したいデータの定義と要相談です。
    2. ひとつは「ヒープに確保されるクラス」です。このタイプは、多態性のある能動的なオブジェクトに適しています。
      絶対に protected: virtual なデストラクタが必要です。 絶対に、です。
      コピーコンストラクタと代入演算子を private: でオーバーロードします。
      演算子はオーバーロードすべきではありません。
      コンストラクタから virtual メンバ関数をコールすることは禁止です。
    3. ひとつは「インターフェイス」です。このタイプは、インスタンス化されず、他のクラスが持つ能力を表現します。
      すべてのコンストラクタは protected: にします。コンストラクタが不要な場合、空のデフォルトコンストラクタを protected: で定義します。
      絶対に protected: virtual なデストラクタが必要です。 絶対に、です。
      static const 以外のメンバ変数を持つことを禁止します。
      すべてのメンバ関数を public: な純粋仮想関数として定義します。
      演算子はオーバーロードすべきではありません。
      実装を持つべきではありません。
  2. 可能な限り、不変にする
    (すべてのメンバが) 不変なオブジェクトは、コンストラクタで妥当性をテストすれば、後はずっと妥当なため、テストの手間が少ないです。また、マルチスレッド環境で排他制御をする必要がなく、Concurrency に利用できます。デッドロックの原因になることもありません。
    メンバ変数は、まず const で修飾するべきです。もし必要な場合は、そのときに const を除去しましょう。
    メンバ関数は、まず const で修飾するべきです。もし必要な場合は、そのときに const を除去するか、const ではないバージョンをオーバーロードしましょう。
    Getter と Setter はペアではありません。Getter を公開しても、条件反射的に Setter を公開しないでください。Setter よりも、機能を実現する関数を公開しましょう。どうしても必要な場合だけ Setter を公開してください。
  3. const メンバ関数で、非 const 参照・ポインタを返してはいけない
    これをやると、const で修飾されている変数のオブジェクトを変更できてしまいます。それは重大な掟破りです。
  4. 引数が1つであるコンストラクタは、explicit で修飾する
    explicit で修飾しなければ、暗黙的なキャストに使われてしまいます。
    暗黙的なキャストは発見し難いバグの原因になります。そのキャストが自明でない限り、暗黙的なキャストは定義しないでください。
  5. STLを活用する
    STL (標準テンプレートライブラリ) のコンテナクラス、アルゴリズムを活用しましょう。
    これらはエキスパートが作った汎用的なクラスで、この上ないほど高速で、高品質です (1部を除いて)。自分で同じものを作るよりもはるかに高性能で簡単なので、何らかの機能を設計する前に、STLにどんなものがあるかを調べてみましょう。

今日はこのくらいで。

Comments are closed.