[ C++で開発 ]
少し込み入ったプログラムを開発していると、ヘッダファイルを2重にincludeしてしまい、コンパイルエラーとなることがあります。ここでは、その回避方法として使われる内部インクルードガードと、include処理時間を大幅に削減しコンパイル時間を短くする冗長インクルードガードの2つを取り上げます。
(追加)最近のGCCでは#pragma onceも無警告で使用できるようになっているようです。
まず、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生成コマンドを実行して結果をコピー&ペーストする手間です。
根本的に解決する手段です。一時期はコンパイラ依存だった機能ですが、最近は主要コンパイラでも提供されているので、レアなコンパイラ系のサポートや移植が想定されない場合はこれを使うのがよいでしょう。
シンボリックリンクには対応できないかもしれません。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 |
計測項目 | 時間 | 備考 |
---|---|---|
平均コンパイル時間 | 11.731秒 | |
リンク時間 | 4.782秒 |
計測項目 | 時間 | 備考 |
---|---|---|
平均コンパイル時間 | 11.884秒 | |
リンク時間 | 12.937秒 |
計測項目 | 時間 | 備考 |
---|---|---|
平均コンパイル時間 | 1.800秒 | |
リンク時間 | 49.079秒 |
計測項目 | 時間 | 備考 |
---|---|---|
平均コンパイル時間 | 1.329秒 | |
リンク時間 | 49.500秒 |
GCC3.2の場合、冗長インクルードによる差はありません。
VC++6.0の場合は、冗長インクルードの方が平均コンパイル時間が速い結果が得られました。
GCCのCプリプロセッサには、インクルードガードで囲まれたヘッダファイルを一度インクルードすると、その後同じマクロが定義されている場合include指令ではそのヘッダファイルの読み込みをスキップする機能があります。そのため、冗長インクルードと同じ効果が得られます。