コードを0から作るとき。
「あれ?ヘッダファイルってどうやってかくんだったっけ?」
となった時の処方箋です。
困ったことにヘッダファイルの書き方をまとめた文献が日本語のものでほとんどありませんでした。なければ書けばいいと思いここにまとめました。
ヘッダファイルの書き方を理解するときのポイントは
ヘッダファイルは開けておくべきスペースを他のオブジェクトファイルに教えることが役割
ということです。すこし、ピンとこない表現かもしれません。
でも、この役割を中心に考えていくとヘッダファイルの書き方をすんなりと飲み込めます。
では、これをおぼえつつヘッダファイルの書き方のtipsをみていきましょう。
ファイルは明確な機能ごとに分割すること。
こんなことありませんか?
matrix.h
というファイルがあったとしましょう。
それを開いてい見たら…
class Vector3
{
/* 省略 */
};
/* 以下、数千行。 */
class Matrix
{
/* 省略 */
// ちょろっと60行ほど。
};
名前からして行列を定義するファイルのはずなのにベクトルの定義が混じり込んでいます。
しかも、ボリュームからしてベクトルの定義だけではないでしょう。なんでもかんでも入っている状態です。どこに何があるかわからないコードです。
保守性のために、
ひとつのファイルにひとつの機能
を心がけましょう。
インクルードガードを忘れないこと。
最近のIDEでは自動的に挿入されることの多いインクルードガード。
主に以下の2つの方法で書かれています。
#ifdef FILE_NAME_HPP
#define FILE_NAME_HPP
#endif
#pragma once
コンパイラが#pragma once
をサポートしているなら後者を使いましょう。
これがないと誤って2回同じファイルをインクルードした時に多重定義エラーが出てコンパイルできません。忘れずに入れましょう。
モジュールを使うのに必要な宣言は全てヘッダファイルに記述されること。そして、モジュールを使うときは必ずそのヘッダファイルを経由すること。
例えば、モジュールを使う側が参照するヘッダとモジュールを開発する人が使うヘッダといったように分けた場合を考えましょう。
開発者用ヘッダ編集したあとに必ずモジュールを使う人のためのヘッダも編集しなければなりません。保守性が著しく下がるのでかならず、上のルールを守りましょう。
編集するファイルは少ないほうがいいです。
プログラム全体にわたるグローバル変数はヘッダファイルでexternをつけて宣言しよう。そして、定義はCファイルに書こう。
グローバル変数の定義をするときによくやってしまうミスです。
int global_val;
これでは、宣言だけで定義がありません。そのため、バイナリにこのグローバル変数の実態がない状態になります。
実行ファイルを作って動作させた時に未定義の動作となります。
正しくはこう書きましょう。
extern int global_val;
int global_val;
定義はヘッダファイルに書かないようにしよう。
ヘッダファイルで関数を定義することができます。
一見良さそうですが、ヘッダファイルに書かれた実装はインラインとしてオブジェクトファイルに展開されます。実態がないためブレイクポイントを貼ることができません。さらに、呼び出されるたびに関数がインライン展開されてバイナリが肥大化していきます。
それだけでなく、変更を加えるたびに参照している.cファイルの再コンパイルが必要です。
実行時のパフォーマンスと保守性が著しく下がります。
必ず定義は.cファイルに書きましょう。
ヘッダファイルでは必要最低限のインクルードを行おう。
インクルードを書くのがめんどくさくて適当に必要そうなヘッダファイルをインクルードしまくることをやる人が多いです。
しかし、これをやってしまうとファイルの依存性をコードから読み解くことが難しくなります。
また、昨今のIDEがサポートしている補完の機能はコードがインクルードしているファイルを参照しています。補完が遅くなるなどIDEのサポートを邪魔します。
不完全な宣言で十分なときはインクルードしないようにしよう。
基本的にはユーザが定義する構造体、クラスなどは宣言がなければ使うことができません。
しかし、型の宣言がなくても使用することができる例外があります。
それは、ポインタ型です。
ポインタ型は指す対象がなんであれ宣言なしでつかえる
ポインタ型は指す対象にかかわらず一定のサイズを持っています。
なので、宣言無しで使うことができます。
この性質を利用すればモジュールの実装を他のモジュールから完全に切り離すことができます。
この性質を利用したもので有名なのがpimpl(実装へのポインター)イディオムです。
これを使えば完全に実装をモジュールの利用者から隠蔽できます。
ヘッダファイルの内容は単独でコンパイルできるようにする。
これを守っていないとモジュールの利用者側でヘッダを使えるようにお膳立てする必要がでてきます。自分が書いたモジュールを使う他のプログラマに恨まれたくなかったらこのルールを守りましょう。
A.cはA.hを始めにインクルードすべき。それ以外のインクルードはその後。
モジュールの依存関係を完全にコントロールするためです。
これを誤ると気づかないうちに依存関係が解決され、本来コンパイルできないものがコンパイルできてしまうことがあります。
#pragma once
Test spawnTest();
#include"test.hpp" // class Testの定義
#include"test_generator.hpp"
Test spawnTest()
{
/* 実装 */
}
このファイルはコンパイルすることができます。
しかし、test_generator.hppを他のモジュールから呼ぶとどうなるでしょうか?
Testの宣言が無いためエラーとなります。
このような気づかないうちの依存の解決を防ぐためインクルードの順序に気をつけましょう。
.cファイルをインクルードしてはいけない。
「え?そんなことできるの?」
という反応が帰ってくると思います。
実はできます。
こういうことをしたくなるのは以下のような時です。
- はじめは普通の.cファイルに実装
- ファイルに実装した関数をやっぱりinlineで定義したい。
- 関数をインライン化
- 関数を他のモジュールで使いたくなってきた。
- ヘッダをインクルードしたらエラー
- あれれ?と思いつつ試しに.cファイルをインクルード
- 動いた。完成☆
言うまでもなくこれは他のプログラマーにとって悪夢です。
インライン関数は実装が無いため、.cファイルと同様に扱うともろもろの問題を引き起こします(関数にブレイクポイントを貼れないなど…)。
この問題を回避するためには違う拡張子を使うことです。この用途でよく使われる拡張子は.incや.inlです。