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

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

std::mutex vs. boost::mutex (1.71.0) vs. Windows CRITICAL_SECTION with VS2019 and Windows 10

現在の Windows 10 で Visual Studio 2019 の C++ プロジェクトので一般的に採用しやすい Mutex の各種の実装の実効速度的な優位性についてのメモです。特に結論とかべき論を展開する気はないのであくまでも参考程度に図ってみたらこういう結果になる事もあった、程度の事です。

条件

今回、CPUに本気を出させていませんが、このメモでは評価対象の実効速度性能を同じ条件で比較できればよいので気にしなくてよいです。

計測用に書いたコードはメモの末尾につけます。内容は、

  • std::pow による計算を Mutex ( std::mutex / boost::mutex / Windows CRITICAL_SECTION ) で排他制御しながら一定回数(=1,000,000)繰り返す負荷」を
  • 「同時に一定数(=8)のスレッドから処理させる計測」を
  • 「ばらつきも見るために一定回数(=8)繰り返す」

という実装にしました。

結果は、 簡単な開発環境のバナー --> 計測前の warm up --> std::mutex --> boost::mutex --> Windows CRITICAL_SECTION の順に出力しています。 calculated value は各計測バッチごとに、負荷による計算の結果(値そのものに意味はありません)を表示しています。 warm up は排他制御していないので計算結果は実行ごとに競合が発生し変化し得ます。計算負荷の中心は{ネイピア数}を{円周率の指数関数で増幅}/{1/円周率の指数関数で減衰}を繰り返す内容です。

Result ( release: /Ox )

[the Benchmark of the Windows Mutices]
_MSC_VER = 1923
_DEBUG = (undefined: RELEASE BUILD)
_WIN32_WINNT = 0x0A00
<warm up>
wall=0.19 user=1.28 system=0.00 total(u+s)=1.28 p(t/w)=680.9% | caclulated value = 2.71828
wall=0.18 user=1.31 system=0.00 total(u+s)=1.31 p(t/w)=718.4% | caclulated value = 2.71828
wall=0.21 user=1.63 system=0.00 total(u+s)=1.63 p(t/w)=786.5% | caclulated value = 2.71828
wall=0.21 user=1.53 system=0.00 total(u+s)=1.53 p(t/w)=741.1% | caclulated value = 2.71828
wall=0.21 user=1.53 system=0.00 total(u+s)=1.53 p(t/w)=743.2% | caclulated value = 23.1407
wall=0.20 user=1.56 system=0.00 total(u+s)=1.56 p(t/w)=788.9% | caclulated value = 2.71828
wall=0.21 user=1.52 system=0.00 total(u+s)=1.52 p(t/w)=734.6% | caclulated value = 2.71828
wall=0.21 user=1.53 system=0.00 total(u+s)=1.53 p(t/w)=742.8% | caclulated value = 2.71828
<std::mutex>
wall=1.00 user=7.48 system=0.00 total(u+s)=7.48 p(t/w)=749.6% | caclulated value = 2.71828
wall=0.89 user=6.92 system=0.00 total(u+s)=6.92 p(t/w)=773.8% | caclulated value = 2.71828
wall=1.07 user=8.17 system=0.00 total(u+s)=8.17 p(t/w)=765.8% | caclulated value = 2.71828
wall=1.01 user=7.70 system=0.00 total(u+s)=7.70 p(t/w)=759.4% | caclulated value = 2.71828
wall=0.94 user=7.11 system=0.00 total(u+s)=7.11 p(t/w)=758.8% | caclulated value = 2.71828
wall=0.90 user=7.06 system=0.00 total(u+s)=7.06 p(t/w)=783.4% | caclulated value = 2.71828
wall=0.93 user=7.19 system=0.00 total(u+s)=7.19 p(t/w)=773.7% | caclulated value = 2.71828
wall=0.93 user=7.16 system=0.00 total(u+s)=7.16 p(t/w)=770.9% | caclulated value = 2.71828
<boost::mutex>
wall=0.91 user=0.53 system=1.03 total(u+s)=1.56 p(t/w)=172.6% | caclulated value = 2.71828
wall=0.91 user=0.64 system=0.89 total(u+s)=1.53 p(t/w)=169.1% | caclulated value = 2.71828
wall=0.91 user=0.73 system=0.80 total(u+s)=1.53 p(t/w)=168.3% | caclulated value = 2.71828
wall=0.88 user=0.69 system=0.78 total(u+s)=1.47 p(t/w)=166.2% | caclulated value = 2.71828
wall=0.91 user=0.67 system=0.91 total(u+s)=1.58 p(t/w)=173.9% | caclulated value = 2.71828
wall=0.90 user=0.83 system=0.63 total(u+s)=1.45 p(t/w)=161.2% | caclulated value = 2.71828
wall=0.90 user=0.66 system=0.98 total(u+s)=1.64 p(t/w)=181.4% | caclulated value = 2.71828
wall=0.91 user=0.58 system=0.94 total(u+s)=1.52 p(t/w)=166.9% | caclulated value = 2.71828
<Windows CRITICAL_SECTION>
wall=1.77 user=6.55 system=3.03 total(u+s)=9.58 p(t/w)=539.8% | caclulated value = 2.71828
wall=1.74 user=5.89 system=3.16 total(u+s)=9.05 p(t/w)=519.4% | caclulated value = 2.71828
wall=1.75 user=5.00 system=3.06 total(u+s)=8.06 p(t/w)=459.9% | caclulated value = 2.71828
wall=1.75 user=6.33 system=3.03 total(u+s)=9.36 p(t/w)=535.1% | caclulated value = 2.71828
wall=1.75 user=6.61 system=2.89 total(u+s)=9.50 p(t/w)=543.9% | caclulated value = 2.71828
wall=1.75 user=5.73 system=3.41 total(u+s)=9.14 p(t/w)=521.9% | caclulated value = 2.71828
wall=1.77 user=5.80 system=3.67 total(u+s)=9.47 p(t/w)=536.4% | caclulated value = 2.71828
wall=1.75 user=6.47 system=3.20 total(u+s)=9.67 p(t/w)=552.8% | caclulated value = 2.71828

これを見た私はこの性能評価をしようと思ったきっかけに対して「ぁー」という感想を得られました。わたしは満足。実用上のヒントとしては十分。いまのところさらにINTERNALは想像以上の興味はないので。

Result ( Debug: /Od )

[the Benchmark of the Windows Mutices]
_MSC_VER = 1923
_DEBUG = (defined: DEBUG BUILD)
_WIN32_WINNT = 0x0A00
<warm up>
wall=0.58 user=4.63 system=0.00 total(u+s)=4.63 p(t/w)=790.6% | caclulated value = 23.1407
wall=0.59 user=4.70 system=0.02 total(u+s)=4.72 p(t/w)=798.1% | caclulated value = 23.1407
wall=0.60 user=4.81 system=0.00 total(u+s)=4.81 p(t/w)=796.2% | caclulated value = 23.1407
wall=0.58 user=4.59 system=0.00 total(u+s)=4.59 p(t/w)=797.4% | caclulated value = 2.71828
wall=0.58 user=4.63 system=0.00 total(u+s)=4.63 p(t/w)=791.5% | caclulated value = 2.71828
wall=0.59 user=4.59 system=0.00 total(u+s)=4.59 p(t/w)=784.9% | caclulated value = 2.71828
wall=0.60 user=4.69 system=0.00 total(u+s)=4.69 p(t/w)=787.7% | caclulated value = 2.71828
wall=0.59 user=4.69 system=0.00 total(u+s)=4.69 p(t/w)=790.2% | caclulated value = 23.1407
<std::mutex>
wall=4.69 user=37.03 system=0.00 total(u+s)=37.03 p(t/w)=788.8% | caclulated value = 2.71828
wall=4.67 user=36.80 system=0.03 total(u+s)=36.83 p(t/w)=788.9% | caclulated value = 2.71828
wall=4.62 user=36.58 system=0.02 total(u+s)=36.59 p(t/w)=791.5% | caclulated value = 2.71828
wall=4.63 user=36.58 system=0.00 total(u+s)=36.58 p(t/w)=789.9% | caclulated value = 2.71828
wall=4.67 user=36.92 system=0.00 total(u+s)=36.92 p(t/w)=790.4% | caclulated value = 2.71828
wall=4.68 user=37.22 system=0.00 total(u+s)=37.22 p(t/w)=794.9% | caclulated value = 2.71828
wall=4.63 user=36.55 system=0.00 total(u+s)=36.55 p(t/w)=789.3% | caclulated value = 2.71828
wall=4.67 user=37.08 system=0.00 total(u+s)=37.08 p(t/w)=793.3% | caclulated value = 2.71828
<boost::mutex>
wall=4.11 user=4.03 system=3.78 total(u+s)=7.81 p(t/w)=190.0% | caclulated value = 2.71828
wall=4.11 user=3.64 system=4.06 total(u+s)=7.70 p(t/w)=187.4% | caclulated value = 2.71828
wall=4.13 user=3.63 system=4.11 total(u+s)=7.73 p(t/w)=187.4% | caclulated value = 2.71828
wall=4.11 user=3.83 system=3.97 total(u+s)=7.80 p(t/w)=189.8% | caclulated value = 2.71828
wall=4.12 user=3.64 system=4.14 total(u+s)=7.78 p(t/w)=188.9% | caclulated value = 2.71828
wall=4.12 user=3.86 system=3.89 total(u+s)=7.75 p(t/w)=188.3% | caclulated value = 2.71828
wall=4.11 user=3.56 system=4.11 total(u+s)=7.67 p(t/w)=186.8% | caclulated value = 2.71828
wall=4.12 user=3.92 system=4.06 total(u+s)=7.98 p(t/w)=194.0% | caclulated value = 2.71828
<Windows CRITICAL_SECTION>
wall=3.62 user=22.45 system=2.83 total(u+s)=25.28 p(t/w)=698.5% | caclulated value = 2.71828
wall=3.60 user=22.27 system=2.91 total(u+s)=25.17 p(t/w)=698.8% | caclulated value = 2.71828
wall=3.63 user=22.47 system=2.81 total(u+s)=25.28 p(t/w)=696.7% | caclulated value = 2.71828
wall=3.65 user=23.31 system=2.77 total(u+s)=26.08 p(t/w)=714.3% | caclulated value = 2.71828
wall=3.63 user=23.52 system=2.20 total(u+s)=25.72 p(t/w)=708.8% | caclulated value = 2.71828
wall=3.64 user=23.55 system=2.63 total(u+s)=26.17 p(t/w)=719.1% | caclulated value = 2.71828
wall=3.62 user=22.28 system=2.92 total(u+s)=25.20 p(t/w)=695.6% | caclulated value = 2.71828
wall=3.61 user=21.94 system=3.14 total(u+s)=25.08 p(t/w)=694.8% | caclulated value = 2.71828

デバッグビルド版は今回のメモの目的ではないのでおまけ参考データ程度です。

この評価に使用したソースコード

Visual Studio 2019/C++: ファイル単位で設定された Configuration はどこにあるかのメモ

このメモを残した経緯

保守性の視点では使わない方がよいのですが、諸事情によりファイル単位で Configuration を施される事はしばしばあります。そして、プロジェクトの責任者が変わり、ドキュメントにも注意が残されず、ファイル単位で特殊な Configuration が行われている事が忘れ去られ、やがて誰かがプロジェクトを保守する際に不可解なエラーによりその存在に気が付く事になります。問題は、不可解なエラーの原因がどこにあるのか探り出すにはそれ相応の手間がかかる事です。

PCH の Use / Create の切り替え程度の一般性も高く、プロジェクトの構成やファイルツリーを見ただけでおおよそ想像がつくような場合は問題になりません。しかし、次のような場合は少々問題の対応に手間が必要です。

問題の例

  1. "昔々"、プロジェクトの黎明期に何らかの理由で Debug 用の Configuration でもいくつかの特定のソースファイルは /O1 で翻訳されるようファイル単位の設定を施しました(†1)
  2. それから何年もの時間が経過し、Visual Studio のバージョンアップやC++言語規格のバージョンアップも何回かあり、プロジェクトの近代化対応が求められるようになります
  3. "今"となっては謎多きプロジェクトの地下に広がる広大なダンジョンを安全に再探索するため、例えばプロジェクトの Congiguration に /RTCc を加えたとします:
1>cl : Command line error D8016 : '/O1' and '/RTCc' command-line options are incompatible
1>C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\IDE\VC\VCTargets\Microsoft.CppCommon.targets(314,5): error MSB6006: "CL.exe" exited with code 2.

"今"の担当者はプロジェクトの Configuration を見直しますが、 C/C++ ⮕ Optimization を確認しても Disabled (/Od) が設定されています。ふぁっきゅー。

と、なるわけです。問題は Output を見ても一体どのファイルがわざわざ Debug ビルドで個別に /O1 を設定されているのかわからない事です。わっざへぇぅ。

  • (†1): 少なからずのプロジェクト黎明期の神話の時代の神々は諸事情によりプロジェクトチームへ小さな約束事を口伝で伝えてはくれますが、まともなドキュメントは残しません。

ファイル単位で Configuration が施されたファイルの探し方

  1. プロジェクトファイル .vcxprojテキストエディターで開きます (†1)
  2. /Project/ItemGroup (†2) を単位として "目grep" を開始します
    • おそらく先頭にある Label 属性を持った ItemGroup 要素はプロジェクト全体の Configuration です
    • <ItemGroup> はプロジェクトの設定のほか、ファイルの種類ごとにいくつか存在します
      • ソースファイル ( .cpp など) が対象の設定を探す場合は /Project/ItemGroup/ClCompile を含む <ItemGroup> を探します
      • 画像ファイル ( .ico など ) が対象の設定を探す場合は /Project/ItemGroup/Image を含む <ItemGroup を探します
      • リソースファイル ( .rc ) が対象の設定を探す場合は /Project/ItemGroup/Image を含む <ItemGroup を探します
    • たいてい後から施されたであろう設定はファイルの終端に近い方で発見できます
    • 探したい設定値やファイルの一部がわかっている場合はそれをヒントに <ItemGropu> から目的の Configuration を定義しているであろう部分を絞り込みます
      • 例えば /O1 を探す場合は MinSpace で探し、
      • /Ox を探す場合は MaxSpeed で探し…
      • つまり、通常ユーザーの見えるところに露出している設定値とは少し値の表現が異なるそれを探す必要があります

例えば↑の問題の例では、↓のような <ItemGroup><ClCompile> 子要素を持ち、その属性で対象となるファイルが指示され、さらにその子要素に目的の Configuration が埋もれています:

  <ItemGroup>
    <ClCompile Include="something.cpp">
      <Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MinSpace</Optimization>
    </ClCompile>
  </ItemGroup>

探し方のコツさえわかれば、あとは目的に応じたファイル単位の Configuration を必要に応じて洗い出し、必要に応じた調整を施せます。もし、設定値の XML での値の表現の変化 ( /O1MinSpace のような ) がわからなければ、適当に小さな新規プロジェクトを作り、構成から x86 と Release を削除して .vcxproj を目grepしやすくした上で、残る唯一の Configuration へ目的の設定値を施した状態で .vcxproj をテキストエディターで開いて観察するとよいかもしれません。たぶん Microsoft もこの部分の詳細な仕様のドキュメントは公開していません。

ファイル1つだけ、構成も複雑ではなく該当の設定箇所を見つけやすい場合はこのように既存の設定をどうにかすることで問題を解決できます。しかし、あまりに構成が複雑怪奇な場合は諦めて完全にまっさらな Configuration あるいは .vcxproj を作り直してプロジェクトのソースを取り込むところから始めた方がかえって低コストになることもあるかもしれません。

  • (†1): ファイルフォーマットは XML に準拠しているのでテキストまたはXMLに特化した適当なエディターで開くとよいです
  • (†2): XPath です。

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 に由来する要素はありません。ぱっと見的に初心者は誤解しかねないので老婆心で追記しておきます。

参考

C++/Windows: WinMain/wWinMain な Windows デスクトップ向けアプリで std::cout / std::wcout とかをコンソールに直接出したいときのメモ

Visual StudioWindows デスクトップアプリを作る場合は WinMain / wWinMain がエントリーポイントのプロジェクトを扱うのが一般的です。コンソールアプリとして作って GLFW から画面を出したり imguiGUIを作ったり、コンソールアプリの実行時に Win32 API を叩いてウィンドウハンドルを操作して…というのは特別理由が無ければふつーはしません。

そうすると、 std::cout / std::wcout はじめ標準出力、標準エラー出力、標準入力を扱う仕組みが "コンソールへ文字列を出したい" という点では期待動作はしなくなります。でも、出したい時は:

// (A1) OutputDebugStringA / OutputDebugStringW
// ; 少し趣旨と違うけれどこれでいいなら、いいんじゃない的な代替手段。使うのかんたん
// #include <debugapi.h> // <-- windows.h に含まれるのでふつーは手いんくるーど要りません
OutputDebugStringW( _T( "ヘルシオ・ホットクック美味しいです♡" ) );
// (A2) std::cout が動く。でも使うまでに少しの手間が必要です
#include <iostream>
#include <fcntl.h> // _O_TEXT
#include <io.h> // _open_osfhandle

void somewhere_in_your_code()
{
    // プロセスにコンソールを割り当てる(ref:A2-1)
    AllocConsole();
    
    // コンソールで使う文字コードをUTF8へ切り替える
    //   切り替えないと日本では ANSI -> CP932 のままになります
    SetConsoleOutputCP(CP_UTF8);

    // 現在の std::cout, std::wcout が接続されたストリーム(=FILE*) stream_std_out を開く
    auto stream_std_out = []()
    {
      // internal-step-1:
      // 標準デバイス(=STD_OUTPUT_HANDLE=標準出力を示すフラグ)の"Windowsにおけるハンドル"(=wh_std_out)を取得
      auto wh_std_out = GetStdHandle(STD_OUTPUT_HANDLE); // (ref:A2-2)

      // internal-step-2:
      // 既存の標準出力のWindowsにおけるハンドル(=wh_std_out)を
      // CRT(C run-time)のファイル・ディスクリプター(=fd_std_out)として開く
      //   note: このファイル・ディスクリプターをRAII的に閉じる必要はありません (ref:A2-2-a)
      auto fd_std_out = _open_osfhandle((intptr_t)wh_std_out, _O_TEXT); // (ref:A2-3)

      // internal-step-3: 
      // 直前に開かれたCRTのファイル・ディスクリプターをCRTのストリーム(FILE*)として開く
      return _fdopen(fd_std_out, "w"); // (ref:A2-4)
    }();

    // 現在の std::cerr, std::wcerr が接続された stream を開く
    auto stream_std_error = []()
    {
      // ↑の cout/wcout と同様
      auto wh_std_error = GetStdHandle(STD_ERROR_HANDLE);
      auto fd_std_error = _open_osfhandle((intptr_t)wh_std_error, _O_TEXT);
      return _fdopen(fd_std_error, "w");
    }();

    // 現在の std::cin, std::wcin が接続された stream を開く
    auto stream_std_in = []()
    {
      // ↑↑の cout/wcout と同様
      auto wh_std_in = GetStdHandle(STD_INPUT_HANDLE);
      auto fd_std_in = _open_osfhandle((intptr_t)wh_std_in, _O_TEXT);
      return _fdopen(fd_std_in, "r");
    }();

    // C++ の標準ストリーム系(iostream系なやつら)を C の標準ストリーム系(cstdio系的なやつら)と同期する
    std::ios::sync_with_stdio( true ); // (ref:A2-5)


    // 現在のCRTストリームに接続されたCRTハンドルを閉じつつ、新たにCRTストリームへCRTハンドルを接続する
    freopen_s(&stream_std_out  , "CONOUT$", "w", stdout); // (ref:A2-6)
    freopen_s(&stream_std_error, "CONOUT$", "w", stderr);
    freopen_s(&stream_std_in   , "CONIN$" , "r", stdin );

#if 0
    // VS2019で著者が試した限りでは不要ですが、VS2005からしばらくでは必要かもしれません。
    // C++ の標準ストリームオブジェクトのエラー状態をクリアーする
    //   有効な標準ストリームが設定される前に扱おうとするとエラーフラグが立つ対策。
    //   VS2005以降ではコンソールへの読み書きの有無によらず常に実行時にエラーフラグが立つので必要「かも」しれません
    //   (著者は古いVSでわざわざ試していないので確認していませんが参考 ref:A2 によると必要な事が起こるかもしれないので一応メモへ残します)
    std::wcout.clear(); std::cout.clear(); // (ref:A2-7)
    std::wcerr.clear(); std::cerr.clear();
    std::wcin.clear();  std::cin.clear();
    std::wclog.clear(); std::clog.clear();
#endif

    // そうして、ようやくですが std::clog など使えるようになります
    std::cout << u8"UTF-8で🍵どーぞ" << std::endl;
    std::cout << u8"⛄🎄📦🎍🌸" << std::endl;
}

f:id:USAGI-WRP:20191207033750p:plain

…悲しい…。(通常の目的は達成できていますが…)

    std::cout << u8"UTF-8で🍵どーぞ" << std::endl;
    std::cout << u8"⛄🎄📦🎍🌸" << std::endl;

↑モダン・ウェブ・ブラウザーは優秀です…(たぶん、すべての絵文字が見えているでしょう…)。

f:id:USAGI-WRP:20191207034806p:plain

↑WSLの端末エミュレーターcmdより優秀です…。

f:id:USAGI-WRP:20191207035842p:plain

Windows Terminal (Preview)-0.7.3382.0 (A-2-8) は惜しい感じです。入力(IMEで「おちゃ」と入力して変換、確定)はハテナ・ダイヤモンドですが、echoからの標準出力の表示では期待動作して絵文字を表示できました。…これがOSのコンソールの標準に置き換わってくれると、内部的には cmd であれ powershell であれでもUNICODEハッピーな絵文字も使えて人生がだいぶ楽しくなると思うので20年以内にそうなってほしいなーくらいにゆるっと期待したいと思います。(現時点では少なくとも「Windowns Terminalは既存のコンソールを置き換えるものではない」的な事をMicrosoftは言ってたような気はします)

若干、はなしがそれましたが、目的は達成できています。

注意点として、 A-2 の方法で出したコンソールのウィンドウには画面右上におなじみのバツ印ボタンがついています。これを押すと、もともとのデスクトップアプリのプロセスも終了します。

APIを駆使してこのボタンを消したり、あるいはそもそもcmdではない端末エミュレーターへ標準出力をパイプしたり、そういう変態的な事もいちおう WindowsAPI を駆使すればたぶんできますが、素直にログウィンドウを作るなりライブラリーでどうにかするなり、ログをファイルかメモリーかデータベースか何れにせよどこかへ出すなりバッファリングするなりしておいて別プロセスで読み出せるようにするなり…。そもそもどうして std::cout とかを出したかったのか、その要求を考え直さないとただの変態的でテクニカルなソフトウェア・モダン・アートのごみができると思います…。

…でも🍣とかログに出したいですよね…2019年にもなって…。ぅぅ…。

おまけ: ↑の状況とはある意味では逆転して、コンソールアプリだけどエラーメッセージのダイアログを出したい、場合には?

// (O1) でんっ!って出るやつです
#include <crtdbg.h> // おまけのおまけ: MSVC++ では <string> ( --> <xstring> --> <iosfwd> --> <yvals.h> --> <crtdbg.h> ) とインクルードされたりもします
_RPTFW0( _CRT_ERROR, _T( "今夜はパッチェリ・アラビアータにしよう。Hは発音しなでくださいとか日本語に求めないでね!" ) );

_RPT マクロ "群" なので、たくさんのマクロのセットです。 W の有無は Widechar/ascii のそれ、F の有無はファイル名と行番号を自動挿入する/しない、末尾の数字はオプションの引数の数、の組み合わせです。第1引数でエラーの他にワーニングも出せます。注意として、この関数はデバッグ版のランタイム・ライブラリーにしか含まれないので、カオスでアーティスティックでテクニカルな事をしない限りは通常デバッグビルド構成でしか使用できません。

f:id:USAGI-WRP:20191207005603p:plain

参考

Visual Studio 2019 C++: Configuration に「絵文字」を使える、というメモ

f:id:USAGI-WRP:20191207072224p:plain

この画面写真↑には3つの構成(Configuration)が含まれています:

  1. Debug
  2. Release
  3. 🍣 tmp exp hoge 🍵

絵文字/everywhere です。上手く使うと構成が多数あったり、似た構成がある場合の間違い防止効果を期待した使い方もできそうです。

f:id:USAGI-WRP:20191207073353p:plain

全ての表示がUNICODE絵文字に対応できているわけではないようです。とはいえ、開発中に最も多く使う冒頭の画面写真の位置にある構成切り替えの表示が対応してくれていれば用途として、絵文字を使用する目的は達成できるので、この辺りはあまり気にしなくてもよいかもしれません。

おまけ追記

VS2017 でも同様にリネーム、表示できました。どこまで昔のバージョンまで対応できるか私はこれ以上確認できませんがあしからず。

Visual Studio 2019 C++: _DEBUG _WINDOWS _WIN32 _WIN64 _UNICODE NDEBUG WIN32 WIN64 UNICODE のメモ

シンボル 自動的に定義される?(predefined macro?) VSが新規作成で構成に追加? そもそも何? 効果ある?(*2)
_DEBUG yes ( msbuild ) yes ( Debug ) デバッグビルドを意味する識別用にmsbuild がプロジェクトの Configuration -> Advanced -> Use Debug Libraries が Yes で /LDd /MDd /MTd が有効になる場合に自動的に定義される。但し、プロジェクト作成時にConfiguration(Debug) -> C/C++ -> Preprocessor -> Preprocessor にも明示的に追記される。 yes
_WINDOWS no (*3) yes Windows向けを意味する識別用の定義。プロジェクト作成時にConfiguration -> C/C++ -> Preprocessor -> Preprocessor に明示的に追記される。(*3) no
_WIN32 yes ( cl ) no Windows の 32-Bit 版または 64-Bit 版で動く事を意味する識別用に cl によって自動的に定義される。 yes
_WIN64 yes ( cl ) no Windows の 64-Bit 版で動く事を意味する識別用に cl によって自動的に定義される。 yes
_UNICODE yes ( msbuild + ps ) (*1) yes UNICODEの使用を意味する識別用にプロジェクト作成時に自動的にプロパティーシートとして設定が追加され、 Configuration -> C/C++ -> Preprocessor -> Preprocessor に追記される %(PreprocessorDefinitions) を介する形で追加される。実行効果的としては Win32 API や _T マクロが W になったりする。 yes
NDEBUG no yes ( Release ) デバッグビルドではない=リリースビルドを意味する識別用の定義。プロジェクト作成時に Configuration -> C/C++ -> Preprocessor -> Preprocessor に明示的に追記される。 yes
WIN32 no yes ( x86 ) Windows の 32-Bit 版向けを意味する識別用の定義。プロジェクト作成時に Configuration -> C/C++ -> Preprocessor -> Preprocessor に明示的に追記される。 (*4) nope, ... yup... (*4)
WIN64 no no WIN32に対応したWIN64の定義について何か仕様があるような気がしてユーザーが定義する事のある何か。 no
UNICODE yes ( msbuild + ps ) (*1) yes UNICODEの使用を意味する識別用に定義される。プロジェクト作成時に _UNICODE と一緒に同様に追加される。 no

(*1): プロジェクト作成後に構成のプリプロセッサーの定義から $(PreprocessorDefinitions) を削除または;区切りを忘れて無効となる書き換えを行ったりすると Character Set に Use Unicode Character Set を設定していても定義されなくなってしまうので少しそのあたりの取り扱いには注意が必要です。

(*2): この列における「効果」は Windows SDK で分岐に使われていたりする効果です。例えば WIN64Visual Studio が直接扱ったり Windows SDK にそれを使うコードが含まれたりはしていませんが、しばしばユーザーが独自に定義して使用している事はあります(たいてい_WIN64を使った方がよいとは言え実際にWIN64が使われているコードもしばしば遭遇します)。

(*3): 一見、 _WIN32_DEBUG のように predefined macro のような気配を持っていますが、違います。Visual Studio はプロジェクト作成時に _WINDOWS を構成へ自動的に追記(VS .net 以降 VS 2019 現在まで)しますが、実際のところ意味はありません(対象プラットフォームが Windowsソースコード#if する用途では _WIN32 が一般的に使われていますし、 predefined macro なのでそちらの方が安心です)。 Windows SDK では内部的に _WINDOWS_ というよく似た定義は使っていますが _WINDOWS は使っていません。

(4): VS2019 の時代では(おそらく)完全に MSDN などの Microsoft 公式の情報がオンライン上に現存しないアンドキュメント化&実行効果的にもおまじない化したゆるふわ CPP 定義のようにも思え…ますが、実は現行の Windows SDK の一部でも実際に使われているので、 Win32 API の一部は WIN32 を明示的に追加定義してビルドしないと期待動作しない可能性があります。具体的に言うと WinSock2.h を使いたい場合とか。WIN32 が未定義の場合、 Windows SDK の一部のヘッダーでは 16-Bit 時代の Windows 向けにコードが分岐したりします。ごく一部の機能を使わなければ WIN32 を定義しなくても現行の Windows SDK のほとんどの機能は問題無く使えます。しかし、執筆現在の時代では、公式な資料がオンライン上に現存 しません(たぶん)(5)。

(*5): 著者の記憶からこの定義の存在について歴史的経緯を思い出してみると、 Visual C++ 6.0 かもう少し後くらいの時代に、それまで WIN32 を使う事になっていた"お約束"をMicrosoftが「これからは_WIN32を使おうよ」と変更した…とかあったような気がしないでもないような…どうだったかな…的な何かです。そういえばもうずいぶん昔、 cygwinmingw の環境から winsock2 を使う必要があったときにちょっとだけ WIN32 に "おこ" した気もしますが…もうほぼ思い出せる記憶はありません…。

WIN32 に関する明確な仕様や歴史的経緯のわかる資料についての情報提供の呼びかけ

私が C++ を"当時のドシロウトなり"でもそれなりに使うようになったのは実質的には Microsoft Visual C++ 6.0 からです。それ以前は Microsoft Visual Basic 4.0 から 6.0 時代までの旧式(.netではない)の Visual BasicWindowsGUI アプリを作っていました。処理速度の限界に耐えられなくなり、 C++ デビューした昔ばなし…はまた機会があればメモを残す事にします。

当時は C++ の言語仕様と VC++ 6.0 の言語標準からのずれがどうこうとごちゃごちゃ言えるほどの知識も無く、CD-ROMで配布されていた MSDN (当時のMSDNは翻訳クオリティーがまともだったので日本語でも読みやすかったし、謎のリンク切れも無く快適だったのです…)が教科書みたいなものでした。

そのような次第なので、私の当時やそれ以前の C++VC++ に関する知識は存在しないか、後付けで勉強したか、または想像が記憶を捏造してしまっている可能性も十分にあります。それで、今回気になったために ↑ のような情報提供の呼びかけをふんわりしてみたのでした。

追記: このメモはなんなの?

VS2019でC++プロジェクトを扱う際に、MSVC++での開発で頻出するCPPマクロ定義について、"じっさい"のところ構成などで明示的に定義する必要がある/ないを判断する参考用です。構成が多数あり複雑になりがちな場合に少しでも保守コストを軽減したい、そんなときに何が要らないか気になったので整理しました。

symlink と Property Sheet で Visual Studio の C++ プロジェクトの構成を整理するメモ

Windows向けアプリの比較的古いプロジェクトを扱う事になると「構成」がカオスで整理したくなる事がしばしばあります。または、これから新しく作るプロジェクトについて、構成が複雑になりそうな場合の参考にもなるかもしれないので、その整理方法についてメモを残しておきます。

このメモでは、

  1. 外部依存ライブラリーのインクルードパスをどのように整理するか? --> symlink がおすすめです
  2. プリプロセッサーの定義をどのように整理するか? --> Visual Studio の Property Sheet の User Macro がおすすめです

について簡単に解説します。

前提

  • Windows 10 でもいまでは symlink を使えます( junction とは別に symlink が使えるようになっています)
    • NTFS 上で自然に扱え、 Git for Windows / Git Bash / cmd / PowerShell などで確認や操作できます
    • 少しだけ注意点があります
  • Visual StudioC++ 開発プロジェクトでも Property Sheet の User Macros を使えます
    • User Macros を使うと msbuild 経由で cl.exe へ任意の Key-Value の組による変数を投げつけられます
    • 少しだけ注意点があります

このメモの対象は、管理する構成、外部依存ライブラリー、ビルド時のプリプロセッサー定義が多いプロジェクト向けです。逆に、例えば、x64向けのDEBUG/RELEASEの2パターンしかなく、外部依存ライブラリーもせいぜい1つか2つ、ソースコードレベルのビルドパターンを分けるための #ifdef 用のフラグも存在しないか、せいぜい1つか2つしかない、そのようなプロジェクトではかえって煩雑になるだけの可能性もあるので、必要を見極めた上で、方法の採否を決めてください。

整理メモ

1. 外部依存ライブラリーのインクルードパスをどのように整理するか?

プロジェクトの構成がカオスに陥っている場合、よくある現象の1つに Additional Include Directories (追加のインクルードディレクトリー)が次のように混乱している事がしばしばあります:

..\..\common\libAAA;C:\Python33\include;library\my_old_mystic_lib;libarry\my_new_lib\include;library2/boost_1_71_0;library2\lpng1637;%(AdditionalIncludeDirectories)

セパレーター文字;で分解すると、

  • ..\..\common\libAAA ← だぶん複数プロジェクトで使いまわしているライブラリー置き場にあるライブラリー
  • C:\Python33\includeWindowsでは"おれおれポジション"な場所へ標準でインストールされるはずのライブラリー
  • library\my_old_mystic_lib ← プロジェクト内で初期に整理された秘伝のおれおれライブラリー
  • libarry\my_new_lib\include ← プロジェクト中期におれおれ的には新設計で追加されたと思われる新型のおれおれライブラリー
  • library2/boost_1_71_0 ← プロジェクトを最近になってテコ入れしようと思って追加したと思われるライブラリー
  • library2\lpng1637 ← プロジェクトをテコ入れしたらユーザーニーズが発生して新たに必要になって追加したと思われるライブラリー
  • %(AdditionalIncludeDirectories)Visual Studio がいつの頃からかつけるようになったよくわからないから触れずにつけてあるおまじない(*1)

と、いった具合です。さらに、プロジェクトの構成がデバッグ、リリースだけではなく、デバッグ・アレ特殊版、リリース・ソレ特殊版、実験用1、実験用2…など増えていて、それにさらにx86、x64があり…。MSVC++のプロジェクトではしばしば、ありがちです。

このメモでは symlink で↑を整理し保守性を改善する方法の提案についてメモを残します。

(*1): %(AdditionalIncludeDirectories) の正体はもちろん"オマジナイ"ではなく意味のある値で、次の (2) で間接的には触れますが、それにフォーカスした詳細の解説はこのメモの趣旨ではないので行いません。

symlink による Additional Include Directories の整理

整理方法:

  1. プロジェクトのルートに include ディレクトリーを作ります
  2. プロジェクトのルートに external_libraries ディレクトリーを作ります
  3. プロジェクトに固有のライブラリー群を external_libraries へ放り込みます
    • 原則的に、できるだけダウンロード等で入手し展開した「そのまま」のディレクトリーを配置するようにします
      • 例えば、 boost や libpng はバージョン番号の付いたディレクトリーを include/boost_1_71_0include/lpng1637 のように配置します
      • できるだけ、この時にいちいちディレクトリーの名前を変更しないように注意します(*1)
  4. include から mklink (cmdなど) または ln (Git Bashなど) 等を使い、 external_libraries (または ..\common\など)へ相対パスsymlink を作成します
    • external_libraries内のライブラリーのディレクトリー構成に include またはヘッダーファイル群が入っているディレクトリーが…
      • 含まれる場合は、その1つ上のパスを(*2)
      • 含まれない場合(直下にヘッダーもソースもごちゃごちゃ置いてある場合など)はそのディレクトリーを(*3)
    • ライブラリーの名称または標準的なディレクトリー名で作成します
      • できるだけ、ファイル単位で必要な .h だけ整理して symlink しようとは考えないようにします(*4)
  5. ライブラリーの管理方法はこのようにする事が開発に関わる人がわかりやすいように README.md などに記しておきます

こうすると、理想的には、 Visual StudioC++ プロジェクトの Additional Include Directory は原則的に全ての構成で共通して

  • .\include;%(AdditionalIncludeDirectories)

だけで済みます(*5)。ライブラリーのバージョンアップのたびにすべての構成の Additional Include Directory の設定値を変更する必要が無くなり、作業コストも減り、構成ごとの設定値を同期し忘れる事故も安全に防げます。

注意点として、この方法に統一したい場合に、 libpng のようにディレクトリーのワンクッションが無くソースコードからの #include <png.h> のように使っていた、あるいは使いたいと考えていた場合、ソースコード#include <png/png.h> のように変更する必要が生じます。これを避けたい場合、ファイル1つだけで済む程度ならばファイル単位の symlink としても事実上の作業コストは変わりませんが、もし複数のファイルをそのように扱う必要がある場合は諦めてワンクッション入れる事をおすすめします。(*4)

なお、外部依存ライブラリーそのものの管理方法も Git Submodule で体系化したりできますが、それは構成の話とは別になるのでこのメモでは触れません。

(*1): ぱっと見の精神衛生を気にすれば boost-1.71.0libpng-1.6.37 あるいは他の統一された命名規則を用意して揃えたくなるかもしれませんが、このメモではおすすめしません。もしそのようなルールを採用する場合は、保守コストが増大したり、複数人の開発者が関わる場合に誤った命名に注意を払う必要が生じたり、そのような面倒の方が大きい事を覚悟した上で導入します。また、もし、同じライブラリーでも複数のバージョンを併存させてビルド構成ごとに分けたい場合などは、 external_libraries/boost/boost_1_71_0 のようにワンクッション入れて整理したくなるかもしれませんが、多くの場合、1つのライブラリーにそれほどたくさんのバージョンを併存させる必要はありませんし、バージョンが併存して扱われている可能性も視認し難くなり、保守コストも増加するので同様にこのメモではおすすめしません。

(*2): 例:

  • include/GLFW -> ../external_libraries/glfw-3.3.bin.WIN64/include GLFW
  • include/xl -> ../external_libraries/libxl-3.8.8.2/include_cpp LibXL

(*3): 例:

  • include/boost -> ../external_libraries/boost_1_71_0 Boost
  • include/png -> ../external_libraries/lpng1637 libpng

(*4): 例えば ligpng のように配布物を展開するとソースとヘッダーが整理されずに配置されている場合などありますが、バージョンの切り替えの際の手間が増え面倒となり、さらにスクリプトを作り半自動化したくなるかもしれません。コスト増加に合理的な理由がない場合はディレクトリー単位で簡単な symlink を用意するだけに留める事をこのメモではおすすめします。どうしても、特定のライブラリーについて直接展開先のディレクトリー直下のファイルを #include <something.h> のように扱いたい場合は、次の (2) でメモを残す Property Sheet に実際のパスを定義する Key-Value を定義した上で、 Additional Include Directories ではその変数を参照させる事で、ライブラリーのバージョンアップのたびに複数の構成の設定値を書き換える手間がかからないようにはできます。

おまけ: Windows や Git Bash で symlink を使える状態に設定にする方法
  1. Windows10を開発者モードにする(設定→更新とセキュリティー→開発者向け(画面左側)→開発者モード)
  2. cmd で setx MSYS winsymlinks:nativestrict
  3. Git Bashgit config --global core.symlinks true

2. プリプロセッサーの定義をどのように整理するか?

構成がカオス化したプロジェクトでは Preprocessor の定義もカオス化していることがしばしばあります。複数のリリースパターンが存在するプロジェクトで #ifdefソースコードレベルでビルドを分岐している場合によく起こります。例えば、

FLAG_X, FLAG_Y, FLAG_P, FLAG_Q, FLAG_R, ... などたくさんのフラグが…

  • X + P ← 初期からある組み合わせ
  • Y + Q ← 初期からある組み合わせ
  • X + P + Q ← ある時点で追加した組み合わせ
  • Y + P + Q + R ← 最近追加した組み合わせ

のようにあり、これらを /D オプション( = Preprocessor の設定値 )で渡してフラグを切り替えた別バージョンとしてビルド、リリースしていた場合、

  • FLAG_S を追加する事にし、これは X + P + Q + S の組み合わせでリリースする事にした…
  • FLAG_Q を廃止する事にした…

そんなような事が起きている事があります。あるいは、デバッグビルドでは特別なフラグを設定して運用している、とか。

何れにせよフラグの管理が合理化されておらず、すべての構成ですべてのフラグを直接管理していると、特にフラグが多ければ多いほど構成の追加や変更に伴いフラグ管理のミスによる誤ったビルド、リリースによる事故が起きたり、そうでなくともたいていセットで使うフラグをプロジェクトの新人がうっかり知らずに片方だけONにしてトラブルになったり…そのような問題に繋がりやすくなります。

Visual Studio の構成の Preprocessor 項のように文字列を与えられる部分ではUser Macros環境変数を使い設定値を整理できます。 (1)で symlink を使ったので、"その手"の手段としては環境変数も使える手段ですが、 WindowsVisual Studiomsbuild を使う場合にはあまり便利ではありません。このメモでは User Macros を使い整理する手法を残します。

User Macros ( Property Manager, Property Sheet, .props ) の仕組みと使い方

  1. View (表示)から Property Manager を表示します
  2. Property ManagerAdd New Project Property Sheet ボタンがあるので、ぽちって MyFlag.props など適当な名前のプロパティーシートを追加します(この名前は簡単に後でも変えられます)
  3. MyFlag のプロパティーVisual Studio で開きます(*1)
  4. User Macros を開き、プロジェクトの構成で整理したい文字列の設定値を作ります(*2)
  5. プロジェクトのプロパティーを開き、(4)で整理(定義)した文字列値を使いたい設定値の部分で $(MY_DEBUG) の形式で使います(*3)
  6. 整理されたフラグはプロパティーマネージャーを使う事を開発に関わる人がわかりやすいように README.md などに記しておきます

こうすると、たくさんの /D オプション用のフラグ管理と構成が必要な場合に、セットで使うはずのフラグの設定漏れをある程度防いだり、構成を追加する最に構成の設定値上で記述するフラグの数を少なくして手間を省いたりできます。

この管理方法を使うと、フラグの削除が必要になった場合の手間はもしかしたら僅かですが増えるかもしれません。ファイル1つからフラグを置換(削除)するか、ファイル2つからするか、というだけの違いなので、何れにしても Visual StudioVSCode の強力で便利でプレビュー付きで安全な正規表現でまとめて処理するのでさほど…とは思いますが、一応書いておきます。

(*1): プロジェクトのプロパティーの画面とほぼ同じなので混乱しそうですが、プロパティーマネージャーの構成とプロジェクトの構成は別に管理されているものなので慌てないでください

(*2): 単純なKey-Valueの文字列変数です。例えば、 MY_DEBUG=_DEBUG;MY_EXTRA_DEBUG_FLAG とか、 FLAG_PQ=FLAG_P;FLAG_Q とか。

(*3): msbuild 向けの "変数的なそれ" の展開方法は3種類あります:

  1. Property element へのアクセス = $(Key) <-- 今回 (2) で Property Sheet の User Macros に追加した Key-Value で使いました
  2. Item element へのアクセス = @(Key)
  3. Item Metadata へのアクセス = %(Key) <-- Visual Studio の作るプロジェクトの構成の Additional Include Directories に標準で追加されている %(AdditionalIncludeDirectories) はこのパターンです (*4)

(*4): プロジェクトの構成にみられる `%(AdditionalIncludeDirectories) はプロジェクトの構成( = .vcxproj)内で循環参照しているわけではなく、別ファイル管理のプロパティーシート( = .props)群の Additional Include Directories 群をバッチ的に展開しています。

参考