ヘッダファイルの記述は最小限に


C/C++言語では、ヘッダファイルにともなうコーディングのしにくさを生じることがある。

本やネットの記事で読んで、自分も実際に有効だと思ったコーディングスタイルをメモする。

前方宣言で十分なときには ヘッダファイルに書かない

・1つのファイルのソースコードの中で、関数を記述する順序を調整することで、宣言を不要にする。

int func1(){
    ...
}

int func2(){
    ...
    func1();
    ...
}

Google C++スタイルガイド 日本語訳
にも同じことが書かれている。

無理に関数の宣言をヘッダファイルに書かない

 関数のプロトタイプ宣言の目的は、他のファイルに記述されている関数でのデータの受け渡し方法を教えることにある。コンパイルしている単独のソースコード以外では、ソースコード中の関数が、どういう引数を受け取り、どういう値を知らないから、それだけを教えるためにある。だから、何もヘッダファイルに書かないで、その関数を参照する側のファイルにプロトタイプ宣言をべた書きしても十分にコンパイルしてビルドできる。
 だから、ヘッダファイルの記述にとても苦しくなってしまったときは、ヘッダファイルに書かず、参照するソースファイルにべた書きすることにしている。そうすることで、コンパイルエラーをまずは避けられる。
 とにかく、プログラムが動かないことには、話にならないからだ。
(「モジュールの設計がよくないから、そうなるのだ。」というお叱りを予定している。しかし、作っている最中には、何が適切な設計なのかを十分にわかっていない場合があるのです。試行錯誤をしやすくするためには、この手法を用いています。)

マクロ定数の記述は、静的に領域確保する際に用いる。(Cの範囲で書く場合)

実は、C++言語の最近の規格では、const int でも静的に領域確保する配列の大きさの指定に使える。

constSize.cpp
#include <stdio.h>
int main(int argc, char* argv[]){
    const int n = 100;
    int data[n];
    for (int i = 0; i < n; i++){
        data[i]=i;
    }
    for (int i = 0; i < n; i++){
        printf("%d %d\n", i, data[i]);
    }
}

このような書き方をするのか、しないのかは、部署でのコーディング指針にしたがっていただくのがいいでしょう。
配列の大きさの記述にはマクロ定数を用いるのが、まだまだ多いですから。
(もちろん、配列をnew, delete で領域を動的に確保するときには、マクロ定数を使う必要はありません。)

マクロ定数を返すためだけの関数・メソッドを作る

 マクロ定数を複数のソースコードファイルに取り込むのにヘッダファイルを使うのは副作用が多すぎます。マクロ定数を返すためだけの関数・メソッドを実装し、それを別のソースコードの中で呼び出すようにすれば、そのマクロ定数は、そのファイルの中では不要になります。

そのマクロ定数が、静的な領域確保に使われるのでなければ、この手法は使えます。

externをヘッダファイルに書かない

externは、その指し示す変数の実体が、ビルドされるソースコードの中に、必ず1つ(しかも1つだけ)存在することを要求します。
そのヘッダファイルを#includeすると、そのヘッダファイルの中に記述されている全てが有効になります。そのため、externで示された変数の実体(しかもグローバル変数)が、ヘッダファイルの側の知らないどこかに、今ビルドするプロジェクトのどこかにあることを要求します。
ですから、そのことを避けるためには、externをヘッダファイルに書かず、externを必要とするソースコード自体にexternをべた書きすることにしています。

ヘッダファイルの依存性をDoxygenで確かめる

自分では、1つのヘッダファイルをincludeしたつもりが、とてつもない範囲のヘッダファイルをincludeしていることがあります。
Doxygenを使ってヘッダファイルの依存関係を描画させることです。そうすることで余分なincludeを見つけやすくすることです。

不要かもしれないincludeは、コメント化して確かめてみよう

このincludeは本当にいるのだろうかと疑問に思ったら、すぐさまコメント化してビルドしてみよう。ビルドできたら、いらなかったので削除する。ビルドできなかったら戻す。これをこまめに行う。
 #includeは、次の理由で余分なものが残ってしまいがちである。
・とりあえず使いそうだと思って書いてしまったが、結局使わなかった。
・最初は必要だっだのだが、書き換えによって不要になってしまったのに、そのまま放置されてしまった。

Pythonの統合環境Spyderの場合だったら、利用されていない変数、利用されていないimportがあると自動で指摘してくれるが、私の使っているC++の統合環境にはそのような機能がないのが残念です。

ヘッダファイルに「これがなければ良いのに」と思ったら

そう思ったら、プロトタイプ宣言はべた書きする。そのヘッダファイルからはとりのぞく。別に必要になる分は、別のヘッダファイルにする。同一のcode1.cppに書いてあるからといって、code1.hに全てを書かなければならないなんてことはない。webの記事にもよく指摘があるように、「これがなければ良いのに」と思ったら分割しよう。分割すれば、依存性が少なくなるため、単体テストにしても格段に容易になる。なんとか1つのヘッダファイルでできないかと試行錯誤して時間を過ごすのはもったいない。

同じ値だからといって、出所の違う定数を無理やり同じマクロ定数にしない

同じマクロ定数として記述しようとすると、不自然なincludeを生じることになる。値が同じかどうかはassert()でチェックするなどして、結果として同じ値になるものでも別々に定義するようにすれば、ヘッダファイルのincludeの関係がむごくなるのを防げるのではないかと思っている。

C/C++言語では、ヘッダファイルは鬼門です。ささいなことで、ビルドできないとかメモリ関係のエラーの原因となります。まだまだ、勉強中です。

付記:

 単体テストすることを考えると、いやおうなしでも意味のまとまりを意識しなければならなくなります。ヘッダファイルの中には単体テストするのに必要十分な宣言だけ含めるようにします。
 単体テストを既存のコードに追加するには、単体テストしやすいインターフェースを持つように、既存の関数を分割する必要(関数の抽出をする必要)が生じてきます。そうすると全体の設計がわかりやすいものになっていく傾向があります。
 一連の設定に関するマクロ定数をひとつのヘッダファイルに含めて、その1つさえ見れば自在にカスタマイズできるというヘッダファイルの作り方はやめた方ががよさそうです。その流儀にしてしまうと、単体テストが非常に困難になります。

欲しいツール
 ヘッダファイルにあるマクロ定数やenum型の宣言のうち、その宣言がどの.cppファイルで使われているのかいないのかを解析するツール。pythonなどでできないものだろうか。

追記(2017年8月):次のqiitaの記事は、不要なincludeを見つけてくれるツールを紹介しています。

Github include-what-you-use

GO言語にふれて思ったこと

「使用していない変数はバグの要因となり、使用していないインポートはコンパイル速度を低下させます。コードツリーに未使用のインポートが増えるととても遅くなります。このため、Go言語ではどちらも許していません。」
というのは、実にいい考えだ。