Windows APIを使ったC++開発では、原因不明のコンパイルエラーや、テンプレートが突然壊れる不可解な現象に遭遇することがあります。
その場合、<Windows.h>が定義するマクロが静かに悪さをしていることがあります。
本記事では、その代表例である min/max マクロがなぜ危険なのか、そしてその対策法について紹介します。
前提知識
-
C++のマクロについて知っている -
Windowsヘッダーを使ったことがある
何が問題なのか
Windows APIを利用するために<Windows.h>をインクルードすると、ヘッダ内部で大量のマクロが自動的に定義されます。
その中でも特に問題になりやすいのが、次の 2 つのマクロです。
#define min(a, b) ((a) < (b) ? (a) : (b))
#define max(a, b) ((a) > (b) ? (a) : (b))
これらはWindowsが古くから提供している便利マクロですが、C++の標準ライブラリが提供するstd::min/std::maxと名前が衝突します。
マクロはプリプロセッサによる強制置換であるため、もし衝突してしまった場合、std::min(x, y)のようなコードが マクロ展開されてしまい、意図しない形に書き換えられてしまいます。
外部ライブラリなどを使用してデスクトップアプリ開発をしている場合、そのライブラリがstd::min()やstd::maxを使用していたために、意図しない形に展開されてしまって謎のコンパイルエラーが出るということがよくあります。
サンプルコード
最も単純なエラーの例
//--------------------------------------------------------------------------
//! @file main.cpp
//! @brief Windows.hの問題点を提示するサンプルコード
//! @author つきの
//--------------------------------------------------------------------------
#include <Windows.h>
#include <algorithm>
// エントリポイント
int main()
{
// 適当に値を用意する
int a = 1;
int b = 2;
// Windows.hのmin()マクロが邪魔して、std::min()が呼び出せない
int c = std::min(a, b);
// プログラムの終了
return 0;
}
重大度レベル コード 説明 プロジェクト ファイル 行 抑制状態 詳細
エラー (アクティブ) E0040 識別子が必要です NOMINMAXTest C:\Users\maihe\Desktop\NOMINMAXTest\NOMINMAXTest\main.cpp 16
重大度レベル コード 説明 プロジェクト ファイル 行 抑制状態 詳細
エラー C2059 構文エラー: ')' NOMINMAXTest C:\Users\maihe\Desktop\NOMINMAXTest\NOMINMAXTest\main.cpp 16
重大度レベル コード 説明 プロジェクト ファイル 行 抑制状態 詳細
エラー C2589 '(': スコープ解決演算子 (::) の右側にあるトークンは使えません。 NOMINMAXTest C:\Users\maihe\Desktop\NOMINMAXTest\NOMINMAXTest\main.cpp 16
具体的なエラーの理由
上記のコードがエラーになる理由は、
std::min(a, b)の中のminが、Windows.hのマクロによって強制的に書き換えられてしまうためです。
C++のコンパイルは**プリプロセッサ(マクロ展開)**→ **コンパイラ(文法解析)**の順で行われます。
今回のmin(a,b)マクロはプリプロセッサによって
#define min(a, b) ((a) < (b) ? (a) : (b))
と展開されます。
つまり
int c = std::min(a, b);
と書いていましたが、プリプロセッサは
int c = std::((a) < (b) ? (a) : (b));
と置き換えたことになります。
こうなるとコンパイラが文法解釈を行う頃には、我々が書いたコードの意図はかき消されてしまっているというわけです。
回避方法
最も確実な対策は、Windows.hがmin/maxマクロを定義しないようにすることです。
そのためにWindows APIは公式にNOMINMAXマクロを用意しています。
NOMINMAX を使った安全なコード例
//--------------------------------------------------------------------------
//! @file main.cpp
//! @brief min/maxマクロを無効化するサンプルコード
//! @author つきの
//--------------------------------------------------------------------------
#define NOMINMAX
#include <Windows.h>
#include <algorithm>
// エントリポイント
int main()
{
// 適当に値を用意する
int a = 1;
int b = 2;
// Windows.hのmin()マクロが邪魔して、std::min()が呼び出せない
int c = std::min(a, b);
// プログラムの終了
return 0;
}
#include <Windows.h>の前にNOMINMAXマクロを定義することで、min/maxマクロが定義されない状態でコンパイルを行うようになります。
知っていても忘れやすく、ヒューマンエラーになる部分なので、実務ではpch.hなどで定義しておくことを強く推奨します。
// 実務でのプリコンパイルの例
#define NOMINMAX
#include <Windows.h>
これでプロジェクト全体がmin/maxマクロの影響を受けなくなります。
総括
-
Windows.hにはマクロが大量に定義されており、コードを破壊する可能性がある - 中でも
min/maxマクロはstd::minやstd::maxと頻繁に衝突をする -
NOMINMAXを定義することで、Windows.hのmin/maxマクロを無効かできる - 実務では
プリコンパイルに含めることでヒューマンエラーを回避しやすくなる