インクルードガード

c++の#include ディレクティブは、プリプロセスで解釈され、書かれたファイル名の内容を単にその場所に展開したものとして処理されます。

 

一般的にファイルの先頭付近にinclude文はまとめて書く感じですが、コードの中に埋め込むことも可能です。

たとえば以下のような、動物の名前を列挙しただけのファイル(animals.csv)があるとします。

馬、狼、カンガルーと書いてあるだけですが

とすると、

と展開されて、普通にコンパイルが通ります。

このように#includeディレクティブはとにかく機械的に展開するものだ、という話です。

で、例えば普通に同じヘッダーファイルを複数includeしてしまうと、(機械的に展開するだけなので)includeの数だけその場所に展開されます。include元のファイルに何かシンボルの定義みたいなものが含まれている場合、多重定義エラーになります。

hoge.hpp

main.cpp

なので、一つのコンパイル単位中に同じファイルが複数回、展開されないようにする必要があります。

多重に展開されることを防止するには、展開元のファイルに多重展開防止用のマクロ(インクルードガード)を仕込むのが定石とされています。

c/c++が標準で提供する全てのヘッダーファイルにはインクルードガードが使われています。

インクルードガード

シンボルの定義名(例ではINCLUDE_GUARD_HOGE_HPP)は、一意に定義される文字列なら別に何でも良いです。

複数includeされても、最初の一回以外はINCLUDE_GUARD_HOGE_HPPが定義済みなので単にスキップされますので、include元のファイル内に書かれたクラス定義などが多重定義されてしまう心配はありません。

冗長インクルードガード

先ほどの例のようにinclude元のファイルの先頭と末尾を#ifndef -#endifで囲うインクルードガードを、厳密には「内部インクルードガード」といいます。

そして、この#ifndef -#endifの部分をinclude先にも書く手法を「冗長インクルードガード」といいます。

このように書くと#include文の評価をする前に、スキップするか決められる為、コンパイル時間の短縮につながる場合があります。

困ったことに、コンパイル時に複数のヘッダーファイルが何度も検出されるような状況下では、ヘッダーファイルを開き、#ifndefを評価し、終わりの#endifを走査するプロセスに時間がかかる可能性があります。場合によっては冗長インクルードガードを設定することにより、コンパイル時間を大幅に短縮できます。

__C++標準的コーディング技法 #62:インクルードガード

冗長インクルードガードについては毎回外部に書く手間の割に大したメリットも無いのであまり使われていません。むしろガードにつかうマクロ名を内部インクルードガードと冗長インクルードガードで合わせなければいけない為、別々のファイルなのに結合が生じてしまいます。内部インクルードガードのマクロ名を変えた場合、冗長インクルード側のマクロ名も忘れずに変えなければなりません。

外部の#includeガードは冗長で、現在のコンパイラ上では時代遅れである。また、インクルードする側とヘッダーファイルがガード名について合意していなければならない。これにより、密接な結合が生じ、脆弱となる。

__C++ Coding Standards

今の時代には不要となった冗長インクルードガードですが、教養として押さえておくと良いでしょう。コモンセンスってやつですね。

#pragma once

言語機能ではありませんが、多くのコンパイラではこの魔法の言葉をファイル中に書いておくだけで複数インクルードされない仕組みを提供しています。インクルードガードを書くより、遥かにタイプ数が少なく済むので単純に楽です。

しかし、#pragma onceによる複数include回避の方法はコンパイラに依存するため、コンパイラによっては使えないこともありますし、コンパイラの実装によっては予期せぬ不具合の原因になる場合があります。

例えば、あるファイルを複数includeしない為に、そのファイルが既にincludeされたことがあるかをコンパイラは覚えておく必要があります。include済みのファイルかどうかを判定するために、ファイル単位に一意のシンボル名を定義するとします。その一意の文字列生成の為にファイルパスを埋め込んでシンボル名を作るような実装になっていた場合、すごーーーく長いパス(c:/user/xxxxxxxxxxxx/development/my_project/tekitou_na_project_name/game/lib/include/group/category/tekitou_na_header_file_name.hpp)の場合、途中でバッサリとパスが切られて、まったく同じシンボル名になって、初めてincludeしたファイルなのにスキップされるという、なかなか原因解明が困難な不具合に襲われる……といったケースがあるかもしれません。

……はい、体験談です。

もともと、言語標準じゃない機能なので私は使わなかったのですが、あの一件以来、さらに避けるようになりました。

コンパイラによる拡張機能で、言語の機能ではないので、これをコード中に書いた瞬間に、そのコードはコンパイラ依存のコードになります。ポータビリティの高いコードを書きたい場合は、単に使わない方が得策です。

#pragma onceを使わなくても、マクロによるインクルードガードで、コンパイラ依存の無い形で実現出来ますからね。

#pragma onceも冗長インクルードガードと同様、includeするファイルの中を見に行く前に、以前にincludeしたことがあるかを判定出来るため、若干コンパイル時間が短くなることが期待できます。

実例

初学者はC++の勉強の一環として、CryENGINEのソースコードを読んだりすると思いますが、その中でもこのインクルードガードに触れることができます。

以下はCryENGINEのあるヘッダの冒頭部分です。

インクルードガードのマクロ名が__で始まっていますが、連続するアンダーバーを含む名前はC++の予約語なので真似するのは絶対に止めましょう。

参照:禁忌の識別子

スーパープログラマ集団でも平気でC++の規格に違反してしまうところを垣間見て勇気を得た所で先に進みます。その下でVCのバージョンを見た上で#pragma onceしているのが分かると思います。

#pragma onceが効く環境では#pragma onceによって、ちょっとでもコンパイル時間を短縮しようというという貪欲な姿勢が見てとれますね。このようにインクルードガードとの合わせ技で攻めるというアプローチもあるので覚えておくと良いかもしれません。

 

まとめ

  1. #include ディレクティブはプリプロセスでファイルを機械的に展開するだけ
  2. 多重includeを防止するためにインクルードガードを書こう
  3. インクルードガードには内部と冗長の2種類がある
  4. コンパイラ拡張の#pragma onceによっても多重includeは防止できる
  5. (おまけ)インクルードガードに使う定義名はc++のルールの中で決めよう
Pocket