Windows向けにC++で開発していると、「Windows.h をインクルードした途端に外部ライブラリがコンパイルできなくなる」というやっかいな問題にぶつかることがあります。この記事では、manifold ライブラリと Windows.h を併用したときに発生するビルドエラーを題材に、原因となる min/max マクロの仕組みと、実務で使える具体的な解決方法・設定例を詳しく解説します。
Windows.h と manifold 併用で起きるビルドエラーの全体像
今回の相談内容を整理すると、次のような状況です。
- Windows 環境(Visual Studio など)で C++ プロジェクトを開発中
- 外部 C++ ライブラリの一つとして manifold を導入
#include <Windows.h>を含むコードをビルドするとエラー発生- コメントで「C++17 以上が必要」「エラーメッセージを全部見せてほしい」「インクルード順では?」といった指摘
実際、この手のトラブルは manifold に限らず、任意の C++ ライブラリで再現し得ます。原因はライブラリではなく、ほぼ常に Windows SDK が定義するマクロにあります。
| 項目 | 内容 |
|---|---|
| 環境 | Windows / Visual Studio / Windows SDK |
| 利用ライブラリ | manifold(C++17 以上が前提) |
| 症状 | Windows.h をインクルードするとコンパイルエラー |
| 根本原因 | Windows.h が定義する min/max マクロとライブラリ側の関数・テンプレートが衝突 |
| 本質的な対処 | NOMINMAX を定義して min/max マクロをそもそも定義させない |
エラーの正体:Windows.h の min/max マクロの罠
問題の主犯は、Windows.h が内部でインクルードする minwindef.h にあります。ここでは、歴史的な事情から次のようなマクロが定義されています。
#define min(a,b) (((a) < (b)) ? (a) : (b))
#define max(a,b) (((a) > (b)) ? (a) : (b))
このマクロが存在すると、C++ の識別子 min や max がすべてマクロ展開されるようになります。具体的には、
- ライブラリ内の
template <typename T> T min(T, T); - 標準ライブラリの
std::min/std::max - C++20 の
std::ranges::minなど
こういったシンボルを使おうとすると、コンパイラが「テンプレートをマクロとして展開しようとして崩壊」し、理解不能なエラーメッセージにつながります。
manifold のような幾何ライブラリは、min や max を多用しがちなので、Windows 固有のマクロと衝突しやすいのです。
| ケース | 期待する挙動 | マクロ衝突時の挙動 |
|---|---|---|
std::min(a, b) | テンプレート関数呼び出し | std::(((a) < (b)) ? (a) : (b)) のように壊れたトークン列になる |
ライブラリ内の min(x, y) | ライブラリ定義の関数・テンプレートに解決される | (((x) < (y)) ? (x) : (y)) に強制変換される |
名前空間付き ns::min() | ns 内の min を参照 | マクロ展開が割り込んで構文エラー・意味不明なエラーになる |
最小再現例で見る「典型的な壊れ方」
manifold を使わなくても、次のような最小コードで同じ問題を再現できます。
#include <Windows.h>
#include <algorithm>
int main()
{
int a = 1;
int b = 2;
// ここで std::min を使いたい
int c = std::min(a, b);
return 0;
}
このコードをビルドすると、MSVC では例えば次のようなエラーが出る場合があります(あくまで一例)。
error C2589: '(<': '::' の右側のトークンが不正ですerror C2039: 'min': 'std' のメンバーではありません
manifold のヘッダ内で min / max をテンプレートとして宣言している場合も同様で、コンパイラがマクロ展開を試みた結果、ファイルの深い行で大量のエラーが一気に噴出します。
最も確実な解決策:NOMINMAX を定義してから Windows.h をインクルード
根本的な解決策はシンプルです。
Windows.h をインクルードする前に NOMINMAX マクロを定義し、min/max マクロ自体を定義させない。
ソースコードでの基本パターン
もっとも分かりやすいのは、Windows API を最初に読み込む翻訳単位で次のように書く方法です。
#ifndef NOMINMAX
# define NOMINMAX
#endif
// 任意(あるとビルド高速化 & 衝突リスク低減)
#ifndef WIN32_LEAN_AND_MEAN
# define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
#include <manifold/manifold.h> // Windows.h 以降の任意のライブラリ
ポイントは、「Windows.h がプロジェクト内のどこで最初にインクルードされるか」を押さえることです。最初の場所より前で NOMINMAX が定義されていれば、その翻訳単位では min/max マクロは生成されません。
Visual Studio プロジェクトで一括設定する方法
実務では、「どのファイルが Windows.h を最初に読むか」を常に意識するのは大変です。特に、
framework.h/pch.hといったプリコンパイル済みヘッダを使っている- サードパーティライブラリが内部で
Windows.hをインクルードしている
このような場合は、プロジェクト設定でコンパイル全体に NOMINMAX を配布してしまうのが一番確実です。
- ソリューション エクスプローラーでプロジェクトを右クリックし「プロパティ」を開く
- [構成プロパティ] → [C/C++] → [プリプロセッサ] を選択
- [プリプロセッサの定義] に次の2つを追加
NOMINMAXWIN32_LEAN_AND_MEAN(任意だが推奨)
- 既存の定義がある場合はセミコロンで区切って追記する(例:
WIN32;_DEBUG;NOMINMAX;WIN32_LEAN_AND_MEAN) - 「OK」で保存し、プロジェクトを クリーン → 再ビルド
これで、プロジェクト内のすべての翻訳単位で NOMINMAX が自動的に有効になり、Windows.h をどこからインクルードしても min/max マクロは定義されません。
CMake プロジェクトでの設定例
CMake を使っている場合は、ターゲットに対してコンパイル定義を追加します。
target_compile_definitions(your_target
PRIVATE
NOMINMAX
WIN32_LEAN_AND_MEAN
)
複数ターゲットがある場合は、Windows API を扱うターゲットすべてに同じ定義を付けておきましょう。特に、manifold をリンクするターゲットでは確実に設定しておくと安全です。
| 手段 | 設定箇所 | メリット | 注意点 |
|---|---|---|---|
ソース上で #define NOMINMAX | 特定の .cpp ファイル | 影響範囲を局所化できる | Windows.h を最初に読むファイルを把握しておく必要がある |
| Visual Studio プロジェクト設定 | プロジェクト全体 | 一度設定すれば全翻訳単位で有効 | 解決できない場合は別プロジェクトの設定漏れを疑う |
CMake の target_compile_definitions | ターゲット単位 | ライブラリごとに細かく切り分け可能 | Windows 専用のターゲットだけに付けるなら条件付き設定にする |
リポジトリ構成で気づきにくいポイント:framework.h / pch.h
既存プロジェクトやサンプルでは、Windows.h はしばしば framework.h や pch.h 経由でインクルードされています。例えば、次のような構成を考えてみましょう。
// pch.h
#pragma once
#include "framework.h"
// framework.h
#pragma once
#include <Windows.h>
この場合、ユーザーコードは単に #include "pch.h" しているだけなので、「どこで Windows.h が読み込まれているか」が分かりにくくなります。さらに、プリコンパイル済みヘッダ(PCH)は一度生成されると、中身を変えても再生成するまで古い内容が使われ続けるという落とし穴もあります。
このような構成では、次のように対処するのが安全です。
- プロジェクト設定(プリプロセッサ定義)で
NOMINMAXを有効にする - もしくは
pch.hの一番上で#define NOMINMAX→ その後にframework.hをインクルード - 変更後に PCH を確実に再生成するため、プロジェクトを一度「クリーン」してからビルド
実際に、あるリポジトリ(例:XyApi)でも、Windows.h が framework.h 経由で最初に取り込まれていたため、framework.h より前で NOMINMAX を定義するか、プロジェクト全体のプリプロセッサ定義で一括設定することで解決しました。
代替策・応急処置:インクルード順やマクロ退避でしのぐ
NOMINMAX を定義するのが最も綺麗な解決策ですが、事情により今すぐプロジェクト設定を変えられない場合もあります。そのようなときに使える「応急処置」をいくつか紹介します。
manifold を Windows.h より前にインクルードする
ライブラリが 自前で min/max を定義→Windows.h が後からマクロを定義 という順番なら、場合によってはコンパイルが通ることがあります。例えば:
#include <manifold/manifold.h>
#include <Windows.h>
ただし、この方法は次の問題があります。
- ライブラリ内部で
Windows.hをインクルードしていると意味がない - 他のヘッダとの依存関係次第では順序を変えられない
- 将来の変更で簡単に再び壊れる
そのため、「一時しのぎ」と割り切り、最終的には NOMINMAX を導入することをおすすめします。
マクロを一時的に退避してからライブラリをインクルード
一部の翻訳単位だけで Windows API と manifold を併用したい場合は、push_macro / pop_macro を使ったテクニックもあります。
#include <Windows.h>
#pragma push_macro("min")
#pragma push_macro("max")
#undef min
#undef max
#include <manifold/manifold.h>
#pragma pop_macro("max")
#pragma pop_macro("min")
このパターンでは、
- 一時的に
min/maxマクロを#undef - manifold ヘッダをインクルード
- 最後に元のマクロ定義を
pop_macroで復元
とすることで、限定的な範囲でマクロ衝突を抑制できます。ただし可読性が下がりやすいので、やはり恒久対応としては NOMINMAX に軍配が上がります。
std::min/std::max を安全に呼び出す小技:かっこでくくる
標準ライブラリの std::min / std::max を使うだけなら、次のような小技も有効です。
int c = (std::min)(a, b);
int d = (std::max)(a, b);
関数名を丸かっこで囲むことで、min / max マクロの展開を難しくし、関数テンプレートとして解決しやすくするテクニックです。これは標準ライブラリ実装側でもよく使われる慣習になっています。
| テクニック | 使い所 | メリット | デメリット |
|---|---|---|---|
NOMINMAX 定義 | プロジェクト全体 | 根本原因を断ち切れる | 設定変更の権限が必要 |
| インクルード順の調整 | 一時しのぎ | 設定変更なしで試せる | 将来の変更に弱い |
push_macro/pop_macro | 特定の翻訳単位 | 局所的に衝突を回避 | コードが読みにくくなる |
(std::min)(a, b) | std::min/std::max のみ | 広く知られたパターン | ライブラリ内の min には効かない |
manifold 利用時に必須の前提:C++17 以上を設定する
今回の相談では、コメントで「C++17 以上が必要」と指摘されていました。manifold は C++17 以降で導入された機能を前提に実装されているため、プロジェクトの言語規格が古いと、それだけでビルドが失敗します。
Visual Studio で C++ 言語標準を設定する
- プロジェクトのプロパティを開く
- [構成プロパティ] → [C/C++] → [言語] を選択
- [C++ 言語標準] を
/std:c++17以上に設定- 可能なら
/std:c++20など、より新しいバージョンでも問題ありません
- 可能なら
- 「OK」で保存し、クリーンビルド
古いプロジェクトテンプレートやサンプルコードでは、言語標準が「既定(/std:c++14 相当)」になっていることも多いため、外部ライブラリを導入するときは真っ先に確認しておきましょう。
CMake で C++17 を指定する
CMake では次のいずれかのパターンを使います。
# プロジェクト全体に対して
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# もしくはターゲットごとに
target_compile_features(your_target PRIVATE cxx_std_17)
いずれの場合も、Windows 固有の NOMINMAX 設定とあわせて、「C++17 以上 + NOMINMAX」 をセットで整えておくと、manifold のようなモダン C++ ライブラリをスムーズに扱えるようになります。
実務でのトラブルシュート手順:何から確認すべきか
同様のビルドエラーに遭遇したときに、効率よく原因に辿り着くための手順を整理しておきます。
- 完全なエラーメッセージを確認
- 最初の1行だけでなく、ファイル名・行番号も含めて全部読む
min/maxが関わっていないか注目する
- 最小再現コードを作る
- manifold のインクルードと
Windows.hだけのプロジェクトを別に作る - それでも再現するなら、ライブラリや Windows SDK 周りが原因
- manifold のインクルードと
- 言語標準のバージョンを確認
- Visual Studio なら
/std:c++17以上になっているか - CMake なら
CMAKE_CXX_STANDARDやtarget_compile_featuresを確認
- Visual Studio なら
- NOMINMAX の有無を確認
- プロジェクトのプリプロセッサ定義に
NOMINMAXが入っているか - プリコンパイル済みヘッダを使っているなら、そこで定義されているか
- プロジェクトのプリプロセッサ定義に
- インクルードの入口を特定
Windows.hがどのファイルから最初に読み込まれているか探すframework.h/pch.h/ 独自ラッパーヘッダなどを確認
| チェック項目 | 確認内容 |
|---|---|
| エラーログ | min / max / Windows.h / minwindef.h が関係していないか |
| C++ 標準 | c++17 以上になっているか |
| プリプロセッサ定義 | NOMINMAX, WIN32_LEAN_AND_MEAN が定義されているか |
| PCH の有無 | pch.h などが Windows.h をインクルードしていないか |
| 再ビルド | クリーン → ビルドの順で実行しているか |
質問するときのコツ:何を添えると解決が早くなるか
Stack Overflow や GitHub Issues、Qiita などで質問するときには、次の情報をセットで添えると、回答者が原因に辿り着きやすくなります。
- 完全なエラーメッセージ(ファイル名・行番号まで含めてコピー&ペースト)
- 最小再現コード
- 可能なら 1 ファイル(1 翻訳単位)で完結するサンプル
Windows.hと manifold のインクルードだけに絞る
- 開発環境
- Visual Studio のバージョンや CMake のバージョン
- Target OS(Windows 10 / 11 など)
- コンパイラオプション
- 特に C++ 言語標準とプリプロセッサ定義
また、ライブラリ固有の使い方で詰まっている場合は、
- そのライブラリの GitHub Issues / Discussions
- 公式ドキュメントの FAQ / Known Issues
を参照・質問するのがもっとも確実です。一方で、今回のような Windows SDK 由来のビルド問題は、Microsoft Q&A や Visual Studio のフォーラム、あるいは一般的な C++ コミュニティ(Stack Overflow など)の方が情報を見つけやすいケースも多いです。
最終チェックリスト:manifold + Windows.h を安全に共存させるために
最後に、manifold に限らず、Windows API と外部 C++ ライブラリを併用するときに押さえておきたいチェックポイントをまとめます。
- プロジェクトに
NOMINMAX(できればWIN32_LEAN_AND_MEANも)を定義したか - C++ 標準を C++17 以上に設定したか
Windows.hを含む 最初の場所より前でNOMINMAXが有効になっているか(PCH を含む)- プリコンパイル済みヘッダを使っている場合、変更後にクリーンビルドしたか
- それでも衝突する箇所には
(std::min)(...)や一時的#undefで局所的に対処したか
一度きちんと整備してしまえば、NOMINMAX の設定は将来のプロジェクトでも再利用できます。新しい外部ライブラリを導入するたびに同じ「min/max マクロ問題」で時間を失わないためにも、この機会に自分のテンプレートプロジェクトや社内標準に組み込んでおくと良いでしょう。
まとめ
- Windows 環境で manifod などの C++ ライブラリを使うときのビルドエラーの多くは、
Windows.hのmin/maxマクロが原因 - 根本解決策は、
Windows.hをインクルードする前にNOMINMAXを定義してマクロ自体を無効化すること - Visual Studio ならプリプロセッサ定義、CMake なら
target_compile_definitionsでプロジェクト全体に設定するのが確実 - あわせて、manifold が要求する C++17 以上をコンパイラオプションで有効化しておく
- どうしてもすぐには設定を変えられない場合は、インクルード順や
push_macro/pop_macro、(std::min)(...)パターンで応急処置も可能
「外部ライブラリが悪い」と思いがちなこの問題ですが、実際には Windows 固有の歴史的な仕様に起因しています。一度仕組みを理解してしまえば、他のライブラリでも同じパターンで素早く解決できるようになります。ぜひ、自分の Windows/C++ プロジェクトの標準設定として NOMINMAX を取り入れてみてください。

コメント