ホーム < ゲームつくろー! < C++踏み込み編 < ヘッダーファイルは慎重に扱わないと危険です
その7 ヘッダーファイルは慎重に扱わないと危険です
ヘッダーファイルは関数やクラスのプロトタイプを宣言するファイルです。今更なんだと思われるかもしれません。しかし、ヘッダーファイルは慎重に扱わないと取り返しのつかないバグを生んでしまいます。それこそシステムが崩壊するほどのバグになることも時にはあります。
ヘッダーファイル軽んずべからず。この章はそういったヘッダーファイルにまつわるお話です。
@ ヘッダーファイルって何なのか?
冒頭でも述べましたように、ヘッダーファイルは関数やクラスのプロトタイプ宣言をするファイルです。プロトタイプ宣言とは、「変数の名前、型」そして「関数名、引数の型、戻り値の型」のみを記述する事を指します。クラスの場合、クラス名と共にクラスが持つメンバ変数やメンバ関数がプロトタイプ宣言となります。私たちがコンパイルする時、コンパイラはヘッダーファイルを順々に解析していき、定義されている関数プロトタイプやクラスをどんどん蓄積していきます。最後まで解析して定義されていないクラスなどが検出されると「そんなクラスは知りません」とエラーを吐き出すわけです。つまり、ヘッダーファイルはすべての関数やクラスの整合性をチェックする役目を成しています。
A ヘッダーファイルに実装部を書くのはだめなのか?
ヘッダー部(.h)は宣言、実装部(.cpp)は実装。至極当然のように感じていますが、こうしなければいけないというわけでは決してありません。ヘッダー部に実装を全部書いてしまっても良いですし、実装部に宣言を持ってきてもちゃんとコンパイラは通ります。ではなぜわざわざ分けるのか?この大きな理由の一つは「隠蔽」のためです。1つのファイルに宣言部と実装部をすべて含めてしまうと、プログラムを丸々公開することになります。複数の人でプログラムを共有する時、これは非常に危険です。ある人が実装部の一部を変えただけで、他の人、また過去にそれを使っていたプログラムの全てが影響を受けてしまいます。オブジェクト指向においては実装部を見せない事自体が基本です。COMに至ってはインターフェイス宣言と実装部すべてをCOMコンポーネントとして隠蔽しているため、宣言コードすら見れません。そのくらい今のプログラムは隠蔽するスタイルとなっているのです。COMほどとはいきませんが、実装部分はスタティックライブラリとしてまとめると他人が覗き見できなくなります。
隠蔽と可変の危険性を避けるために、ヘッダーファイルと実装ファイル(ライブラリ)はやはり分けて作成するべきです。
B ヘッダーファイルの罠「重複定義」
ヘッダーファイルはファイルごとに分かれています。どのファイルにどんな宣言がされているかは、コンパイルするまで分かりません。ですから、全く同じプロトタイプ宣言が2つ以上のファイルでされている可能性も出てくるわけです。この「重複定義」をコンパイラは認めていません。
「重複定義」となるもう1つのルートがあります。それは「同じヘッダーファイルを2つ以上の別のファイルがインクルードする」というものです。このやっかいなところは、それぞれのファイルにしてみたら、重複先のヘッダーファイルを呼んでいるだけに過ぎないという点にあります。各ファイルは何も悪い事をしていないのに、それが集まると重複定義エラーになってしまうわけですから、厄介です。
この重複定義を避ける方法は幾つかあります。自分が定義した関数やクラスと全く同じ宣言を誰かがしてしまう危険を回避するためには「namespaceキーワード」を用います。これは、自分が宣言した名前空間(スコープ)で関数を定義することができます。例えば、
namespace TEST{
void Func();
}
namespace TEST2{
void Func();
}
とすると、2つのFunc関数は「TEST::Func()」「TEST2::Func()」というスコープ解決演算子によってきっぱりと分ける事が可能になります。独自のライブラリを作成しようと思ったら、namespaceキーワードは必須と言えるでしょう。
1つのヘッダーを複数のファイルが呼び出す重複定義は、呼び出し先のヘッダーファイルで対処するしかありません。この対処法には2つあります。1つは「#defineマクロ」による個別化です。これは次のように定義します。
#ifndef H_MYCLASS
#define H_MYCLASS
class MyClass
{
};
#endif
#ifndefディレクティブは、その後ろにつけたマクロが定義されていなければ#endifまでの間をコンパイルします。上のヘッダーファイルが最初に読み込まれた時、H_MYCLASSはまだ宣言されていないので、クラスの宣言部がコンパイラに告げられます。2回目以降はもうH_MYCLASSは存在しているので、このクラス宣言が読まれることはありません。これにより、重複定義が回避されるわけです。
対処法の2つ目は「#pragma once」ディレクティブです。これは、1度読み込まれたヘッダーファイルを記憶しておいてくれます。2回目以降にヘッダーファイルが呼ばれた時には、その読み込み自体を無視してくれます。#pragma onceディレクティブは次のように使います。
#pragma once
class MyClass
{
};
こちらの方がマクロ名をいちいち定義しなくて良い分楽です。最近の統合環境ではデフォルトでこのディレクティブが付記されるようになっているようです。ただし、#pragma onceディレクティブの動作は環境に依存します。
兎にも角にも、namespace、#defineマクロもしくは#pragma onceディレクティブをヘッダーに宣言して重複定義を極力避けるというのも、ヘッダー作成において必須作業であることは間違いありません。
C 循環参照はなるべく避けて、そうなる時は慎重に対処
ヘッダーファイルAがヘッダーファイルBをインクルードし、ヘッダーファイルBがヘッダーファイルAをインクルードする事を「循環参照」と言い、非常に厄介な問題を引き起こします。
ClassA.h ClassB.h #pragma once
#include "ClassB"
class ClassA
{
ClassB B;
};
#pragma once
#include "ClassA"
class ClassB
{
ClassA A;
};
次のような2つのファイルを作成してコンパイルしてみてください。きっとコンパイルエラーになると思います。ClassAの中にはClassBのオブジェクトが、ClassBにはClassAのオブジェクトがあります。この場合、ClassAのサイズを決めるにはClassBの大きさが必要になるのですが、ClassBもClassAのサイズを必要とするため、お互いのサイズが決まらずエラーとなります。厳しいのが#pragma onceが付いていても循環参照エラーは解消されないという点です。循環参照は重複定義と同等の問題ではないんです。
循環参照は厄介なんです。重複定義よりもはるかに面倒な側面を持っています。例えば、ClassA→ClassB→ClassC→ClassAという循環が生じてもコンパイラは解決してくれません。複雑に樹状に繋がったヘッダーファイルのどこか一部でもくっついて循環してしまうと、コンパイラエラーが発生してしまいます。しかも、そのエラーを取るのは恐ろしく大変なのです。
実体を持つとサイズが決まらない。ではれば、ポインタを持つのはどうか?これは、一工夫することで多くの場合うまく行きます。
ClassA.h ClassB.h #pragma once
#include "ClassB"
class ClassB;
class ClassA
{
ClassB *pB;
};
#pragma once
#include "ClassA"
class ClassA;
class ClassB
{
ClassA *pA;
};
「class ClassB;」のように、事前にそういうクラスが存在する事をコンパイラに教えてあげるのです(事前宣言)。こうすると、コンパイラはそういう型がそのうち定義されるんだろうと踏みます。クラスのメンバ変数はポインタでして、このサイズはどんな型だろうと4バイトです(環境によりますが一定です)。ですから、クラス宣言もありお互いのサイズも決まりますので、コンパイラエラーにはならないんです。ちなみに、実体を持ってしまっている場合は、上のような事前宣言をしても一切だめです。問題解決にはなりません。
「でも、#include "ClassB.h"としているのにコンパイラは読み込みに行ってくれないの?」と思われるかもしれません。コンパイラの詳しい挙動は私も良くわからないのですが、単純に読みに行っているわけでは無いようです。この辺りに詳しい方がおられましたら是非ご教授下さい。
「インクルードしてるのにコンパイラに型が無いとか言われる」とお困りの方は、@ 循環参照しているクラスのオブジェクト(実体)を持ってしまっている、A ポインタにしているが事前宣言をしていない、のどちらかである可能性が高いのでチェックしてみてください。
D #includeをまとめる時は慎重に!
Cで説明した循環参照を起こしやすいのが「ヘッダー宣言ヘッダーファイル」です。これは、複数のヘッダーファイルをまとめて宣言しているヘッダーファイルで、例えば次のようなものがそれに相当します。
MyClassADef.h #include <MyClassA.h>
#include <MyClassATool.h>
#include <MyClassGlobal.h>
#include <MyClassParameters.h>
こういうヘッダーファイルは、ライブラリを使うユーザが沢山あるヘッダー宣言をいちいちせずに済むように楽をする目的で作成されることが殆どです。ところが、この楽な方法をライブラリ自身が使おうとすると循環参照になります。例えば、
MyClassA.h #include <MyClassADef.h>
#include <CMyClassTool.h>
class CMyClassTool;
class CMyClassA
{
CMyClassTool *p;
};
とすると、MyClassADef.hを読みに行きますが、そこにはMyClassA.hがありますので循環参照になります。しかも、適当にMyClassATool.hなどを個別にインクルードしてしまうと、2重定義も発生します。他のクラスがMyClassADef.hを使ってごにょごにょすると、もうわけが分からないスパゲッティヘッダー宣言となってしまいます。そして、ある日突然「不明の型があります」という類の事を言われます。これは構文エラーでもリンカエラーでもなくてヘッダーファイルの関連性のエラーですから、たぶん泣くほど辛い修復作業に追われることになります(体験者語る(T_T))。
教訓は「ヘッダー宣言ヘッダーファイルをライブラリは絶対に参照しない!」ということです。l面倒でも1つ1つヘッダーをチェックして、慎重にインクルードすることが大切です。
E typedefに#includeはいらない!
自分独自の型宣言をするtypedefキーワードなどで、余計なおせっかいをすると、本当に無駄なおせっかいになってしまいます。
TypeDef.h #include <MyClassA.h>
#include <MyClassATool.h>
#include <vector>
using namespace std;
class CMyClassA;
class CMyClassAtool;
typedef vector<CMyClassA> VctClassA;
typedef vector<CMyClassATool> VctClassATool;
例えば上のようにtypedefのために各クラスのヘッダーファイルを呼んだとします。何となく安心感もありますし、コンパイラも通ります。しかし、この独自の型を使おうとして、
MyClassA.h #include <TypeDef.h>
class CMyClassA
{
protected:
VctClassATool m_VctTool;
};
とtypedefしたヘッダーファイルを読み込むと循環参照が発生します。TypeDef.hの中でMyClass.hが呼ばれていますよね。これは、本当に気が付きにくいバグになります。
typedefキーワードは単なる型変換ですから、実はヘッダーファイルはいりません。ですから、
TypeDef.h #include <vector>
using namespace std;
class CMyClassA;
class CMyClassAtool;
typedef vector<CMyClassA> VctClassA;
typedef vector<CMyClassATool> VctClassATool;
で十分です(#include<vector>も事前宣言すれば実はいらない!)。
typedefする時の事前宣言はとても便利でして、型チェックを要する型限定テンプレートクラスなどでも使えるんです。
TypeDef.h class CMyClassA;
typedef ComPtr<CMyClassA> ComClassA;
例えば、ComPtrテンプレートはAddRef関数、Release関数そしてQueryInterface関数という3つの関数を持っているクラスだけを扱えるとします(COMですね)。しかし、もしCMyClassAクラスがこれらの関数を持っていなくても、このTypeDefヘッダーファイル自身はコンパイラを通ります。やっぱり「typedefは単なる型変換」なんです。ただし、どこかの実装でCpmClassA型を使用した時初めて型チェックが入り、コンパイラエラーが表示されます。
F 1ヘッダーファイル1クラスにしない方が楽
1つのヘッダーファイルで1つのクラスを宣言するスタイルは、VC++やVisual Studioなどの統合環境ではおなじみです。しかし、クラスが膨大になってくるとこの管理が恐ろしく大変になってきます。ちょっとしたライブラリを作成すれば、インターフェイスやそれを実装したクラスの数は簡単に100というレベルを超えます。新しくプロジェクトを立ち上げた時に、ユーザが20も30もヘッダーファイルをインクルードするのは酷です。
その1つの解決策がヘッダー宣言ヘッダーなんですが、これは気をつけないと循環参照になってしまいますし、沢山のヘッダーを扱う危険性はやはりはらみます。ある程度共通性のあるクラス(1つのシステムを構築するクラス群など)は1つのヘッダーファイルにまとめて宣言した方が扱いがはるかに楽になります。ヘッダーファイルの数を減らすと循環参照になる危険性も減りますし、関連性のバグが起こったときも対処しやすくなります。
「でも、そんなことをしたら再利用性が下がってしまうんじゃないか?必要の無いクラスを宣言するのは無駄だ。」と思われるかもしれません。しかし、これは当てはまりません。クラスは必要ならば再度使えば良いだけの話しです。必要の無いクラスを宣言しても、それを使わなければコンパイラは実行ファイルにそれを組み込んだりはしません。もしそれがあるならば、DirectXのゲームはどれもGBレベルになるはずです。
管理面やヘッダー間の関連を考えると、ヘッダーファイルは少ない方が楽です。Direct3Dのルートヘッダーファイルであるd3dx9.hを一度ご覧になると、その意味が良くわかると思います。
ヘッダーファイルはオブジェクト指向において今や必須のファイルとなりましたが、その扱いを軽んじると大変痛い目にあいます。関連性のバグは構文エラーでもなくリンカエラーでもありません。それはそれは辛いバグ取りになります。「ヘッダーファイルの扱いはくれぐれも慎重に」です。