C++ ときどき ごはん、わりとてぃーぶれいく☆

Wonder Rabbit Projectのなかのひとのブログ。主にC++。

VS2019/C++: stdafx.h stdafx.cpp または pch.h pch.cpp つまり「プリコンパイル済みヘッダー」( Precompiled Headers ) の使い方のメモ

このメモの経緯

VSのPCH(PreCompiled Headers)機能は "使うためにユーザーがする事" はもうたぶん20年以上昔から変わっていないと思うので "いまさら" なのですが、ぼちぼち使い方がおかしくてビルド時間を無駄にしているプロジェクトに関わります。そこで、ごく簡単にPCHの使い方についてメモを残す事にしました。

PCHは本来、正しく使えている限りにおいては「たいていの一般的なプロジェクトで何度も開発中に繰り返されるビルド時間を大幅に圧縮する効果を見込める便利機能」です。但し、正しい使い方を知らない人がプロジェクトへ参加して、わけもわからないまま関連のファイルや設定を弄ってしまうと、ほんの少し正しくない使い方をしただけでも本来とは逆効果にビルド時間が増加するだけ&ライブラリーの管理に謎の面倒さが生じたりする厄介者扱いになり得てしまいます。

PCH の正しい使い方

Note: 特別な構成設定を作りこんでいない限りは、以下で Configuration の設定値を変更する前には、対象を Debug や Release などの個別の Configuration ではなく All Configurations に変更して、まとめて設定を行います。そうしない場合、構成ごとに同じ設定を何度も行う事になり単純作業の手間が増えます。(†1)

PCH を使いたいプロジェクトに対して:

  1. Configuration (構成) の C/C++ ⮕ Precompiled Headers ⮕
    1. Precompiled Header を Use (/Yu) に設定
    2. Precompiled Header File を pch.h に設定(†2)
    3. Precompiled Header Output File を $(IntDir)$(TargetName).pch に設定(†3)
  2. (1-b) で設定した pch.h ファイルを作成(中身は空のままでOK)
  3. (2) に対応した pch.cpp ファイルを作成(中身は空のままでOK)(†4)
  4. (3) で作成したソースファイルだけのプロパティーを開き Configuration を (1) と同様に辿り /Yu/Yc に変更(†5)
  5. プロジェクト単位の Configuration の Advanced で Forced Include File /FI[name]pch.h;%(ForcedIncludeFiles) を設定(†6)

これでプロジェクトのリビルドに成功すれば PCH の初期設定は無事に完了したと分かります。

無事に完了した場合は <string><vector> あるいは外部依存ライブラリーの重たいヘッダーファイルをプロジェクトのソースのあちこちで使っても、それらヘッダーファイル側の内容に変更が生じない限りはそれなりに高速にプロジェクトをビルドできるようになります。

PCH を設定した際の手順 (2) で作成した pch.h<string><vector> など、プロジェクトのどこかしらで #include している "内容の安定しているヘッダーファイル" の #include 定義を放り込みます。使用頻度は気にせず、プロジェクトで使う "内容の安定しているヘッダーファイル" は基本的に全て遠慮なく放り込んで構いません。

PCH の正しい使い方を維持する上で必要な注意点は実は1つだけです: 「内容が変化しやすい何かを加えないこと」

心配か、あるいは正しく設定できている気がするのに PCH が正しく動作していないと考えるような状況に陥ってしまっていれば、次項の「 PCH の誤った使い方」 を参考にして下さい。

  • (†1): 通常はプロジェクトで PCH を使う場合、特定の構成に設定する必要はありません。もちろん、 PCH はビルドの構成ごとに個別に動作する必要がありますが、 Visual Studio のプロジェクトではそうしたビルド中の中間ファイルや出力が混ざらないように $(Platform)$(Configuration) を変数としつつ構成を跨いで共通の設定が施された状態でプロジェクトが作成されます。もし、 Configuration ⮕ General の Output Directory や Intermediate Directory でそうした変数を用いないように必要に迫られて変更を施してある場合は、構成事に PCH の関連ファイルが混ざらないように個別に設定するか、改めて $(Platform)$(Configuration) を採用するか、何れかの対応が必要です。通常は $(Platform)$(Configuration) を素直に使い、 Configuration ごとの設定値の差異はできるだけ最小限とする事を保守コスト都合から著者はおすすめします。

  • (†2): pch.h は設定の「例」です。

    • お好みの名前を設定しても大丈夫です。
    • 古い Visual StudioMFC プロジェクト等では標準で stdafx.h 、 VS2019 のデスクトップアプリのプロジェクト等では pch.h がプロジェクトの新規作成時に設定済みになっています(たぶん)。
  • (†3): $(IntDir)$(TargetName).pch は設定の「例」で Visual Studio で作成したプロジェクトに初期設定される値と同じです。

    • お好みのパスを設定しても大丈夫です。
    • このファイルはビルド全体から見れば「中間ファイル」の1つなので、特別な理由が無ければ $(IntDir) の中へ出力します。通常 $(IntDir)$(Platform)\$(Configuration)\
  • (†4): MSDN を含め、おそらく巷の書店の入門書、あるいはこのブログのような技術的なメモのような多くの情報断片、そうしたもので PCH について解説を読むと、このファイルの中身は空ではなく1行だけ #include "pch.h" を記述するよう記されている事が多いと思います。私がこのメモで残す手順と手法ではそれは不要で、他の情報源でも慎重に内容を読めば同様にそれが最終的には多くの場合には不要だった事にも気づけると思います。他の情報源が誤っているわけではありませんし、おそらくそれらは PCH ならまずは PCH についての仕組みついて基礎を忠実に紹介する事を主眼としているだけです。このメモでは実用性を主眼としているため、そのような解説都合上の手順はすっ飛ばしています。

  • (†5): Visual Studio の Configuration は、ほとんどの場合にはソリューションエクスプローラーからプロジェクトを右クリックしてコンテキストメニューからプロパティーを開き、 "プロジェクト単位" で設定を調整します。しかし、ここで必要となるように、実はプロジェクトに含まれる "ファイル単位" でも Configuration を調整できます。

  • (†6): この設定の対象には、もちろん (3) で作成する pch.cpp も含まれます。この /FI オプションは PCH とは別の機能ですが、 PCH に併せて使うと非常に合理的で、結果的に PCH の設定時だけでなく、それ以降もプロジェクトで PCH を使う決断をした事で生じ得る保守コストの増加をおおむね 0 となる程度まで防げます。この設定を利用できない場合は PCH を使うプロジェクト内のすべてのソースコードファイルの先頭に #include "pch.h" を書き加え保守し続ける必要が生じます。その際は pch.cpp にも同様に #include "pch.h" を書く必要があります。

PCH の誤った使い方

このメモのここから先は、

  • 「気づける開発者がプロジェクトに参加していて、そうした中堅以上の技術者がコードレビューに参加できており、開発は Git 等で管理され変更ごとに branch からの pull-request によるマージプロセスを…」

そのような "まとも" な開発状態が理想的に継続できている場合は「そんなうっかりしないよ」的な内容ではあります。

"現実はなんとかよりかんとか"、と世間ではよく言われるようです。しばしば私もそれに同意します。

Bad case 1: pch.h ( stdafx.h ) を「プロジェクト内で共通するヘッダーファイルをまとめるところ」と勘違いしてる

違います:

  • ×「プロジェクト内で共通するヘッダーファイルをまとめるところ」
  • ◎「プロジェクト内で使用する 内容の安定したヘッダーファイル をまとめるところ」

です。

このケースが該当しそうな具体的な例としては、

  • ヘッダーファイルの内容の更新頻度の高い外部依存ライブラリーを pch.h#include している
  • 「プロジェクト内の複数個所で使うから」という理由でプロジェクト内やソリューション内で開発中の機能のヘッダーファイルを pch.h#include している

のようなことが想定されます。

プロジェクトごとに取り扱うヘッダーファイルの "更新頻度" の捉え方は異なりますが、著者の感覚としては…

  • × 数時間、数日、あるいは数週間程度ごとにヘッダーファイルの内容が機能追加や仕様変更によって変化するヘッダーファイル
    • 開発初期の黎明期にある個人が開発を管理するライブラリーの多く
    • プロジェクト内で開発中、あるいはしばしば調整の行われているヘッダーファイルなど
  • △ 数週間ごとにしばしばヘッダーファイルの内容が機能追加や仕様変更によって変化するヘッダーファイル
    • 開発が軌道にのり継続されていて、機能追加 Issue への対応が頻繁な、開発規模が比較的小規模なライブラリーの多く
  • 〇 数か月ごとにまとまった更新が発生する一般的に多くの安定した計画性をもって開発が行われているライブラリーのヘッダーファイル
    • Boost や GLFW や Eigen など多くの比較的規模の大きな OSS ライブラリーなど
    • Microsoft PPL や Qt などのIDEと事実上セットで提供されているような開発環境ごとに準標準的な大きなライブラリーなど
    • Windows SDK<windows.h> など
  • ◎ せいぜい数か月か数年ごとにバグ修正や脆弱性修正が行われる程度になった実質的に開発か終了して仕様が変化しない良い意味で枯れたライブラリーのヘッダーファイル
    • xerces や curlpp など
  • ◎ 標準ライブラリーのヘッダーファイル
    • <string><cstdio> など

×/△は pch.h へ入れてはダメという事ではなく、入れるのが誤った判断となる可能性が高い、という事です。例えば、プロジェクトに必要十分な機能が既に揃っていて、プロジェクト内からは更新する必要がないのなら、入れる選択肢もあります。

〇/◎は pch.h へ入れなければならない、という事ではなく、入れるのが正しい判断となる可能性が高い、という事です。例えば、長い間仕様が安定していた OpenSSL も脆弱性の問題から破壊的変更を伴う仕様変更を受け入れる必要が生じたり、修正頻度が × ほど上がる時期もありました。そういう事があれば、プロジェクトの PCH の対象からはさっくりと外して様子を見る事にしつつ、依存ライブラリーの脆弱性やバグ修正の対応にプロジェクトも素早く追従できるよう調整できるのが理想的には望ましいと著者は思います。

ImageMagick は master ブランチの更新頻度を見れば ×/△ ですが、多くの場合常に最新の master をプロジェクトへ取り込む必要はなく、事実上は安定していると判断できます。また、 Boost や OpenCV のように開発はアクティブでもメジャーバージョンやマイナーバージョンの区切りごとに扱いやすい提供形態がある場合に、プロジェクトが安定したバージョン系列で十分ならば PCH へ含めるのに適していますし、そうではなくプロジェクトから Beta や Alpha の開発ブランチを使いたいのであれば、もちろん PCH へ含めるのは誤りとなります。

Bad case 2: ビルドごとに内容が変化する"定数"を含んだヘッダーファイルを取り込んでいる

先ず、わかりやすい例から。

例えば、何らかの定数をヘッダーファイルが持つ場合:

  • ◎ 「真空中の光速」や「プランク定数」あるいは「π」のような硬い不変性を持つ定数
    • ⮕ 事実上問題になりません、 PCH へ入れましょう
  • 〇 開発中のアプリ独自の許容誤差範囲の定義 1.0e-686400 * 7 (秒単位の1週間) のような安定した定数
    • ⮕ たいていビルド頻度に対してそれらを見直す必要の生じる頻度はとても少ないのでたぶん大丈夫です、 PCH へ入れましょう
      • 但し、その値は安定しているにしても、開発に伴いヘッダーファイルへそのような定数の追加がしばしば行われるヘッダーファイルは×です
  • △ 諸事情により算出された調整用の定数値を含んでいる
    • ⮕ しばしば"再調整"が生じて変更されるようなものはダメです、 PCH からは除外して下さい
  • × プログラム的には定数ではあるが、ビルドごとに変化するバージョン番号や日付と時刻を定数として取り込んでいる
    • ⮕ ダメです、 PCH からは除外して下さい

もう少し分かりにくい例へ。

  • △ 構成単位で切り替えられる /D / #define で与えられるフラグや定数値を含んでいる
  • × 開発中にしばしば調整される /D / #define で与えられるフラグや定数値を含んでいる

しばしば比較的小さくない規模のプロジェクトや歴史の長いプロジェクトでは複数の /D / #define によりビルドを分岐している事があります。そのようなプロジェクトで PCH を導入する場合は事前に日常的な開発で取り扱う可能性のある /D / #define の組み合わせ毎に Configuration を整理しておき、かつそれらの Configuration で PCH が混ざらないように $(IntDir), $(Platform), $(Configuration) 等を扱うか、あるいは PCH を構成毎に個別のファイルとして生成するように設定しなければ PCH について誤った使い方となります。

このメモの著者が遭遇しやすい例では、開発中に用いる Configuration Debug の中で開発中の便利などにより切り替えるフラグ値を #define で管理していたり、その歴史的経緯を含んだまま PCH の運用を前提とせず何らかの都合で $(IntDir) を構成ごとに分けずに共有化してしまっていたり、そのような事がしばしばあります。結果、そうしたプロジェクトで PCH を採用するか、あるいは既にしていたとしても、正しい使い方にならない、あるいはなる時もあればならない時もありビルド時間が時折非常に長く感じられる事がある…、そのような事態に陥っている事があります。

これらは直接ヘッダーファイルがそのようなコードを含む場合にはたいてい未然に PCH への追加を回避されますが、ヘッダーファイルのインクルードツリーから間接的に含んでしまっている場合があります。(†1)

  • (†1): ヘッダーファイルのインクルードツリーは Visual Studio では Configuration ⮕ C/C++ ⮕ Advanced ⮕ Show Includes を Yes( /showincludes ) に設定した構成でビルドすると Output へ出力され、確認できます。

おまけ解説

  • 名称 $(IntDir)
    • たぶん "Intermediates-Directory" 的な発想が元になっていて、「中間生成物用置き場」の意味です。
    • 近年では採用される事の多くなった $(TargetName) のようないわゆる "長いが読んだら元のフレーズが明確にわかりやすい命名" になっていない理由は、おそらくこの変数が Visual Studio に登場した時代にはまだ一般的だった"変数名やファイル名は詰めて短縮/あるいはルールを決めて記号化して短くした方が計算機の都合には優しい"的な考え(その根拠はその時代よりもさらに何十年も昔のメモリーもCPUパワーもリソースがごく限られた時代に必要だった事情によります)
      • 整数型の int に由来する要素はありません。ぱっと見的に初心者は誤解しかねないので老婆心で追記しておきます。

参考