その14 ドキドキしないプリコンパイル済みヘッダー
ちょっとしたテストやサンプルの為に書き捨てるプログラムではなく、大規模なプログラムを作成するとコンパイル時間が馬鹿にならなくなってきます。商用ゲームだと、規模は様々ですが、コンシューマ機で販売するような規模のプログラムになるとフルビルドで数時間からヘタをすると1日近くかかる場合もあります。ゲームプログラマになって初めて超絶に大規模なコードに触れ、さらにそれをフルビルドした時には、それはそれは延々と続くビルドにただただ驚いたのを覚えています。
そこまで大規模とは言わないまでも、ある程度規模が大きくなるとビルド時間を短縮したくなってくるものです。そういう時、割とお手軽で効果的にビルド時間を短くしてくれる仕組みが「プリコンパイル済みヘッダー」です。
C++はコードをコンパイルしリンクすることで実行可能な.exeが作成されます。Visual Studioでソースコードをビルドすると、最初に.cppファイルが出力ウィンドウにどばどばと出てきます。あれはコードを解析してオブジェクトファイル(.obj)を作成する作業(コンパイル)を見ています。オブジェクトファイルとは、テキストなプログラム(.cpp)を機械がわかるマシン語に変換したバイナリファイルの断片です。あくまでも一つの.cppに含まれるコードを変換したものなので、その.cpp内で参照されている他の.hで宣言されている関数(外部参照)はそこに含まれません。その外部参照関係を解決し断片なコードをつなぎ合わせるのが「リンク」です。
ビルドで圧倒的に時間がかかるのはリンクよりもコンパイルです。膨大なテキストコードを誤りが無いか解析してマシン語に翻訳し、外部参照を整理するわけですから大変です。あるコードにちょっとでも変更が入ると、その変更に影響される.cppすべてがもう一度洗いなおされコンパイルし直しが発生します。変更が発生するコード(Unstableコード)があると、前ソースコードに対してその洗い出し作業が毎回発生してしまうわけです。
では、もし変更される事がまず無い安定したコード(Stableコード)がたっぷり出来たとします。そのコードは洗い出しされる必要は本来ありません。でも、変更が発生するとそういう安定コードも原則無差別に洗い出しの対象にされてしまいます。「プリコンパイル」は、その安定したコードの洗い出し作業を前もって(Pre-)行う(Compile)事で、コード変更時の洗い出し作業を変更コード界隈のみにしてくれる仕組みです。ライブラリが育って行くと安定したコードがどんどん増えていきます。そこは見ずに(すでに洗いだしてしまって)開発最中の変更コードのみにコンパイル作業を集中する事ができるのですから、コンパイル時間が飛躍的に短縮できるわけです。
プリコンパイル済みヘッダーというのは、その前もって洗い出してしまった情報をどばーーっと格納した特殊なファイルを指し、通常は「.pch」という拡張子のファイルになります。Visual Studioでプロジェクトを立ち上げるとデフォルトではこの.pchを作成するようにコンパイルオプションが設定されていますので、何も考えずとも.pchが出来ます。プロジェクトフォルダ下にある.objを格納しているDebugとかReleaseフォルダに一緒にあるはずです。最初からプリコンパイル済みヘッダーを使用している場合、それ程問題は起こりません。
一方、途中までプリコンパイル済みヘッダーを使用せずにプログラムを作成していて、コンパイルが激重になってからプリコンパイル済みヘッダーを導入しようとすると、「うぐ!」っというハマりに遭うことがあります。例えば大量に「stdafx.hがありません」というコンパイルエラーが出たり、「プリコンパイル済みヘッダーを作成し直して下さい」のようなエラーが出て、知らないとうまく解決できず泣きそうになるんです。Visual Studioが自動的に設定してくれるコンパイルオプションを手動で設定するのですから、そこにはプリコンパイル済みヘッダーを使うお作法が沢山あるわけです。
@ プリコンパイル済みヘッダーのお作法
ここではVisual Studioでプリコンパイル済みヘッダーを使っていないプロジェクトに対して、後付けでプリコンパイル済みヘッダーを使用する場合のお作法を見てみます。
冒頭で説明したように、プリコンパイル済みヘッダーファイルを作るには「洗い出ししなくていいよ〜」という安定した実装ファイル群(.cpp)をコンパイラに教えてあげないといけません。それを担う役目をしているのが「stdafx.h」というヘッダーファイルです。このヘッダーファイルに安定しているコードを指すヘッダーをずらずらーっと並べると、それがプリコンパイルされる対象になります。プリコンパイル済みヘッダーを使わないで開発をしている場合、こういうヘッダーは通常ありませんので新規で作る必要があります。
「stadfx.h」をパスが通っているフォルダに作成します。Visual Studioはデフォルトでは確実にパスが通っているプロジェクトフォルダの直下にこのヘッダーを作成してくれますが、これは別にパスさえ通っていればどこにあっても構いません。ちなみに、名前を必ず「stdafx.h」にする必要も実はありません。mypch.hでもhogehoge.hでも良いのですが、VSのデフォルトである「stdafx.h」にしておいた方が、識別の意味でも間違いが無いかもしれません。
stdafx.hで宣言したヘッダー群はあくまでも宣言しただけです。それは「使われないと」プリコンパイル済みになりません。それを明示的に使うのがstdafx.cppです。この実装ファイルの中でstdafx.hをぽつんとインクルードします:
stdafx.cpp #include "stdafx.h"
これも別にstdafx.cppという名前である必要はありませんが、従っておいた方がより確実です。このstdafx.cppはプロジェクトに追加が必要になります。
この段階でコンパイルしても、stdafx.hは単なるヘッダーをまとめたヘッダー、stdafx.cppはそれを使っているコードに過ぎず、プリコンパイル済みヘッダーファイルは作成されません。今度はプロジェクトのプロパティを調整して、「stdafx.hがプリコンパイル済みヘッダーファイルを作るための宣言をまとめているヘッダーファイルなんだー!」という事をコンパイラに教えてあげる必要があります。
プロジェクトのプロパティにある「C/C++」->「プリコンパイル済みヘッダー」を選択すると、右側にプリコンパイル済みヘッダーの使用状況が出てくるはずです。この表記はVisual Studioのバージョンで若干異なるのですが、根幹は一緒です。ここではVisual Studio 2005以降の場合で説明します。プロジェクトでプリコンパイル済みヘッダーを使っていない場合、「プリコンパイル済みヘッダーを使用しない」になっているはずです。それを確認したら、ここを「プリコンパイル済みヘッダーを使用する(/Yu)」に変更します。すると「ファイルを使用してPCHを作成/使用」の欄にStdAfx.hが、「プリコンパイル済みヘッダーファイル」に「$(IntDir)\$(TargetName).pch」というのが自動挿入されたと思います。/Yuでプリコンパイル済みヘッダーファイルを使う事を宣言し、.pchを作るのにstdafx.hを使用することをコンパイラに教え、.pchを指定のフォルダに作成するわけです。わかりやすいですね。これでコンパイラはstdafxhを利用してコンパイル済みのファイル(.pch)を$(IntDir)\$(TargetName).pchに作成してくれます。
stdafx.hではなく別の名前にした場合、また.pchを別のフォルダに作成したい場合は、ここを適宜手動で変換します。
さ〜これで一安心、と思ってしまいそうになるのですが、実はここにハマりがあります。
プロジェクトのプロパティでプリコンパイル済みヘッダーを使用すると宣言すると、プロジェクトに登録してある全.cppでstdafx.hを使用しなければならないという制約が課せられます。つまり、.cppの一番ど頭に「#include "stdafx.h"」というインクルード宣言を延々と追加していかなければならないんです。これは、.cppが山のようになると涙が出そうな作業になります。でも、これをしないといけないんです。諦めてコピペを繰り返して下さい。
(↑について、そうしなくても良い方法をとらきちさんよりご教授頂きました。方法は後述します)
全.cppにstdafx.hをインクルードし終わった後、最後の変更が一つ必要になります。それはstdafx.cppのみ「プリコンパイル済みヘッダーを作成する」に変更する作業です。先のプロジェクトのプロパティでは「プリコンパイル済みヘッダーを使用する」としていました。つまり、プリコンパイル済みヘッダーがすでにあるものとしているわけです。ニワトリと卵よろしく、どこかで.pchを一度は作成しなければいかんのです。それを一手に担うのがstdafx.cppです。
stdafx.cppのみ「使用する」ではなくて「作成する」に変更するには、ソリューションエクスプローラ(.cppや.hがずらっと並んでいるウィンドウ)からstdafx.cppを探し出し、右クリックして「プロパティ」を選択します。するとstdafx.cppの個別設定が出てきます。先程と同様に「C/C++」->「プリコンパイル済みヘッダー」を選ぶと「プリコンパイル済みヘッダーを使用する(/Yu)」になっていると思います。これを「プリコンパイル済みヘッダーを作成する(/Yc)」に変更します。
これをしなかった場合、もし事前に.pchが作成されていたら:
プリコンパイルがらみのエラー 1>main.cpp
1>c:\***\***\main.cpp : error C2859: c:\***\***\debug\vc80.pdb は、このプリコンパイル済みヘッダーが作成されたときに使用されたファイル pdb ではありません。プリコンパイル済みヘッダーを再作成してください。
というコンパイラエラーが出ます。また事前に.pchすら無い場合は、
プリコンパイルがらみのエラー 1>main.cpp
1>c:\***\***\main.cpp : fatal error C1083: プリコンパイル済みヘッダー ファイルを開けません。'Debug\HogeProject.pch': No such file or directory
という類のエラーが出ます。いずれもstdafx.cppでプリコンパイル済みヘッダーを「使用する」になっている為に起こるエラーです。
ちゃんとstdafx.cppの個別設定を「作成する」に変更し、プログラムをビルドすると、プリコンパイル済みヘッダーファイルが指定のフォルダに作成されます。最初の1回目のビルドはプリコンパイル済みヘッダーファイルを作成するため通常より少し遅いビルドになるのですが、2回目以降はその分のコンパイルをすっ飛ばすため、大変高速になります。
実は、私このプリコンパイル済みヘッダーが苦手でした。うまく設定できなくて、何だか泣きそうなコンパイルエラーが「どばー!」っと出るんです。いつもドキドキだったのですが、それは色々と知らずハマっていたんだという事がわかり、今はすっきりしています。ここに備忘録を付けておきましたので、これでもうドキドキすることはありません(^-^)
A .cppに#include "stdafx.h"コピペ地獄を回避する方法
@で「プリコンパイル済みヘッダーを使用すると全.cppにstdafx.hをインクルードする必要がある」とし、.cppに「#include "stdafx.h"」を延々とコピペする作業が必要と説明しましたが、とらきちさんより「プロジェクトのプロパティの設定で回避できますよ」という目から鱗な情報を頂きました!・・・作業してしまった私は、別の意味で目から涙でございます(T-T)とほほ。知らないとは不幸なものですねぇ・・・
プロジェクトのプロパティで[C/C++]->[詳細]にある[必ずインクルード]という力強い設定項目にstdafx.hを追加します。こうすると、全.cpp内でstdafx.hが必ずインクルードされるため、先のコピペ地獄を回避する事ができます。
ご情報を賜りましたとらきちさんに、深く深く感謝致します!私は…ライブラリの#include "stdafx.h"を消す地獄をこれからやります(T-T)とほほほ