[ C++で開発 ]

インクルードガード

少し込み入ったプログラムを開発していると、ヘッダファイルを2重にincludeしてしまい、コンパイルエラーとなることがあります。ここでは、その回避方法として使われる内部インクルードガードと、include処理時間を大幅に削減しコンパイル時間を短くする冗長インクルードガードの2つを取り上げます。

(追加)最近のGCCでは#pragma onceも無警告で使用できるようになっているようです。

インクルードガードの必要性

2重インクルードの発生

まず、2重インクルードが発生する例を見てみます。

// Date.h
class Date {
  int year;
  int month;
  int day;
  // ...
};
// Age.h
#include "Date.h"
class Age {
  Date birthDay;
  // ...
};
// Employee.h
#include "Date.h"
#include "Age.h"
class Employee {
  Date employDay;
  Age age;
  // ...
};
"Date.h"をインクルードした時点でclass Dateが定義されています。
次に、"Age.h"をインクルードしたら、Age.hからDate.hがインクルードされているため、class Dateが二重に定義されることになります。

上記のような非常に単純な構成の場合は、例えばEmployee.hでは、Age.hが内部でDate.hをインクルードしていることが分かっているので、Age.hだけインクルードすることで2重インクルードを回避できます。しかし、本格的にプログラムを開発していれば、ヘッダファイルも何百、何千になってきます。そのような場合に対応できなくなります。

内部インクルードガード

上記の場合、2回目にインクルードされたときは、ヘッダファイルの中をスキップするようにプリプロセッサを使って制御する、インクルードガードを使うテクニックが一般的です。

// Date.h
#ifndef INCLUDED_DATE
#define INCLUDED_DATE

class Date {
  int year;
  int month;
  int day;
  // ...
};

#endif
Date.hが最初にインクルードされるときは、INCLUDED_DATEが未定義なため、Date.h内に記述した宣言、定義が有効となります。
2重にインクルードされるときは、2回目以降はすでにINCLUDED_DATEが定義されるため、Date.h内の定義や宣言記述は読み飛ばされます。
// Age.h
#ifndef INCLUDED_AGE
#define INCLUDED_AGE

#include "Date.h"
class Age {
  Date birthDay;
  // ...
};

#endif
同上
// Employee.h
#ifndef INCLUDED_EMPLOYEE
#define INCLUDED_EMPLOYEE

#include "Date.h"
#include "Age.h"
class Employee {
  Date employDay;
  Age age;
  // ...
};

#endif
Date.hをインクルードする時点で、Date.hの中が展開されます。
Age.hをインクルードすると、Age.hの中のインクルードでDate.hを読み込みますが、すでにINCLUDED_DATEが定義されているため読み飛ばされ、Age.h自体の中が展開されます。

インクルードガードに使用するプリプロセッサのシンボル名

この例では、INCLUDED_ + "ヘッダファイル名の拡張子を除去し、残りを大文字とした文字列"のルールを使っています。このルールは、プロジェクトで一貫していれば良いと思いますが、C/C++標準ライブラリやOS・サードパーティのライブラリが提供するヘッダファイルで使用しているシンボル名と衝突しないようにする必要があります。

システムやコンパイラが提供するヘッダファイルでは、システム用として予約されているアンダースコア('_')が先頭についたり二重アンダースコア('__')が含まれるシンボルが使われているので、C++の規格に沿ってシンボル名を使用すれば重なることはないと思います。C++の規格に沿ったシンボル名は以下です。

C++の規格によって、以下の名前は実装系用に予約されているため、インクルードガード名には使用できません。

よく標準ヘッダファイルを見て真似て、アプリケーションで_MYAPP_H_とかを使ってたりしますが、これはNGです。運悪く標準ヘッダファイルの中に同じヘッダファイル名が存在していると大変なことになります。

サードパーティのライブラリのヘッダファイルではどのようなインクルードガードのシンボル名を使用しているかまちまちです。また、大規模になるとプロジェクト内のヘッダファイル同士も名前の衝突の危険が生じます。

そこで、名前空間まで含めたシンボル名を付けてもよいでしょう。

標準ライブラリのインクルードガード・シンボル名の例

標準ヘッダファイルについて、いくつかのコンパイラを調べてみました。やはり、コンパイラによってまちまちなシンボルが使われています。

GCC 3.2 Visual C++ Borland C++ Compiler 5.5
iostream
#ifndef _CPP_IOSTREAM
#define _CPP_IOSTREAM   1
     :
#endif
#ifndef _IOSTREAM_
#define _IOSTREAM_
    :
#endif /* _IOSTREAM_ */
#ifndef __STD_IOSTREAM__
#endif // __STD_IOSTREAM_
    :
#define __STD_IOSTREAM__

UUIDによるシンボル名衝突回避

UUIDを使えば、まず衝突することはありません。弊害はヘッダーファイルを書くときにUUID生成コマンドを実行して結果をコピー&ペーストする手間です。

#pragma onceによるシンボル名使用の不要化

根本的に解決する手段です。一時期はコンパイラ依存だった機能ですが、最近は主要コンパイラでも提供されているので、レアなコンパイラ系のサポートや移植が想定されない場合はこれを使うのがよいでしょう。

シンボリックリンクには対応できないかもしれません。GCC 4.1.2ではOK。

Visual C++では、#pragma once というものもつかわれています。しかし、これはあるバージョンまでのGCCでは、"obsolute"であると警告が出ます。コンパイラによって使えたり使えなかったりしますので、特定コンパイラでしかコンパイルできないプログラムをどうしても書きたい人を除いては使わない方がよいです。

GCCも最近は#pragma onceをサポートしているので、上述のシンボル名衝突を考慮すると、これからは積極的に使ってもよいと考えます。

冗長インクルードガード

内部インクルードガードによって、2重インクルードは防止できるようになりました。しかし、これはコンパイルエラーを防止するだけの処置です。コンパイル時に、コンパイラは多重にインクルードファイルを読み込んではスキップするという処理を延々と実行しています。ヘッダファイルの数が増えれば増えるほど、指数関数的にコンパイル時間が増大していきます。

// Age.h
#ifndef INCLUDED_AGE
#define INCLUDED_AGE

#ifndef INCLUDED_DATE
#include "Date.h"
#endif

class Age {
  Date birthDay;
  // ...
};

#endif
Date.hを既にインクルードしていれば、INCLUDED_DATEが定義されているので、インクルード自体を行わないようにします。
// Employee.h
#ifndef INCLUDED_EMPLOYEE
#define INCLUDED_EMPLOYEE

#ifndef INCLUDED_DATE
#include "Date.h"
#endif

#ifndef INCLUDED_AGE
#include "Age.h"
#endif

class Employee {
  Date employDay;
  Age age;
  // ...
};

#endif
Date.hを既にインクルードしていれば、INCLUDED_DATEが定義されているので、インクルード自体を行わないようにします

Age.hについても同様

冗長インクルードでは、1つのインクルードを行うのに3行の記述が必要になります。ちょっと見づらいですね。しかし、この記述によって、コンパイル時間が大幅に減少します。

サードパーティ・ライブラリの冗長インクルードガード

プロジェクトでシンボル名の付け方を規定しても、サードパーティのライブラリのヘッダファイルは既に異なるルールでインクルードガードが記述されています。そのときは、次のように冗長インクルードガードを記述します。

#ifndef INCLUDED_MATH
#include "math.h"
#define INCLUDED_MATH
#endif
サードパーティライブラリのmath.hをインクルードしたら、プロジェクトの規約に基づいたインクルードガードシンボル名を定義する。

冗長インクルードガードはどこに記述する?

ヘッダファイル中でインクルードしている個所に記述します。ソーステキストファイル(.cなど)には冗長インクルードを使う必要はありません。ソーステキストファイルのインクルード処理時間はO(N)だからです。

冗長インクルードガードの効果を計測する

冗長インクルードガードは果たしてどれほど有効でしょうか。

計測環境

計測マシンのスペック
CPU AMD Athlon XP 2500+ 内部クロック1.83MHz, FSB333MHz
1次キャッシュ128KB、2次キャッシュ512KB
メモリ SD2700 512MB
OS Windows2000 SP4

計測のプログラム構成

100個のソースファイルをコンパイル・リンクして時間を計測します。各ソースファイルは、1つのヘッダファイルをインクルードしています。下図ではU200.ccがC200.hをインクルードしています。このインクルードされたヘッダファイル(C200.h)は、別に100個のインクルード(C100.h 〜 C199.h)をしています。このインクルードファイル群はそれぞれ100個のインクルード(C000.h〜C099.h)をしています。

つまり、一つのソースファイルがインクルードしているヘッダファイルは、100×100で1万個のインクルードを行っていることになります。

U200.cc C200.h C100.h C000.h string.h
vector.h
C099.h
C101.h
C199.h
U201.cc C201.h C100.h C000.h
  :
C099.h
C199.h
U299.cc C299.h

計測結果 〜 GCC3.2-3(Cygwin) 〜

冗長インクルードガードなし
計測項目 時間 備考
平均コンパイル時間 11.731秒
リンク時間 4.782秒
冗長インクルードガードあり
計測項目 時間 備考
平均コンパイル時間 11.884秒
リンク時間 12.937秒

計測結果 〜 Visual C++ 6.0 〜

冗長インクルードガードなし
計測項目 時間 備考
平均コンパイル時間 1.800秒
リンク時間 49.079秒
冗長インクルードガードあり
計測項目 時間 備考
平均コンパイル時間 1.329秒
リンク時間 49.500秒

結果と考察

GCC3.2の場合、冗長インクルードによる差はありません。

VC++6.0の場合は、冗長インクルードの方が平均コンパイル時間が速い結果が得られました。

GCCのCプリプロセッサには、インクルードガードで囲まれたヘッダファイルを一度インクルードすると、その後同じマクロが定義されている場合include指令ではそのヘッダファイルの読み込みをスキップする機能があります。そのため、冗長インクルードと同じ効果が得られます。