新しい MSVC プリプロセッサの概要
Visual Studio 2015 では、標準 C++ または C99 に準拠していない従来のプリプロセッサが使用されています。 Visual Studio 2019 バージョン 16.5 以降では、C++20 標準の新しいプリプロセッサ サポートはフィーチャー コンプリートです。 これらの変更は、/Zc:preprocessor コンパイラ スイッチを使用して利用できます。 新しいプリプロセッサの実験的なバージョンは、Visual Studio 2017 バージョン 15.8 以降で、/experimental:preprocessor コンパイラ スイッチを使用して利用できます。 Visual Studio 2017 および Visual Studio 2019 での新しいプリプロセッサの使用に関する詳細情報が利用可能です。 優先するバージョンの Visual Studio のドキュメントを表示するには、 [バージョン] セレクター コントロールを使用します。 このページの目次の一番上にあります。
Microsoft は、Microsoft C++ プリプロセッサを更新して、標準への準拠を改善し、長年のバグを修正し、正式に定義されていない一部の動作を変更しています。 マクロ定義のエラーについて警告する新しい診断も追加しています。
Visual Studio 2019 バージョン 16.5 以降では、C++20 標準のプリプロセッサ サポートはフィーチャー コンプリートです。 これらの変更は、/Zc:preprocessor コンパイラ スイッチを使用して利用できます。 新しいプリプロセッサの実験的なバージョンは、Visual Studio 2017 バージョン 15.8 以降の以前のバージョンで利用できます。 これを有効にするには、/experimental:preprocessor コンパイラ スイッチを使用します。 既定のプリプロセッサの動作は、以前のバージョンと同じままです。
新しい定義済みマクロ
コンパイル時に使用されているプリプロセッサを検出できます。 定義済みマクロ _MSVC_TRADITIONAL
の値を確認して、従来のプリプロセッサが使用されていないかを確認します。 このマクロは、呼び出されるプリプロセッサに関係なく、それをサポートするコンパイラのバージョンによって無条件に設定されます。 従来のプリプロセッサの場合、その値は 1 です。 準拠するプリプロセッサの場合は 0 です。
#if !defined(_MSVC_TRADITIONAL) || _MSVC_TRADITIONAL
// Logic using the traditional preprocessor
#else
// Logic using cross-platform compatible preprocessor
#endif
新しいプリプロセッサでの動作の変更
新しいプリプロセッサの最初の作業では、すべてのマクロ展開が標準に準拠させることに重点が置かれています。 これにより、従来の動作によって現在ブロックされているライブラリで MSVC コンパイラを使用できます。 更新したプリプロセッサを実際のプロジェクトでテストしました。 見つかった、より一般的な破壊的な変更のいくつかを次に示します。
マクロ コメント
従来のプリプロセッサは、プリプロセッサ トークンではなく、文字バッファーに基づいています。 これにより、準拠するプリプロセッサでは機能しない、次のプリプロセッサ コメントのトリックなどの異常な動作が可能になります。
#if DISAPPEAR
#define DISAPPEARING_TYPE /##/
#else
#define DISAPPEARING_TYPE int
#endif
// myVal disappears when DISAPPEARING_TYPE is turned into a comment
DISAPPEARING_TYPE myVal;
標準に準拠する修正は、適切な #ifdef/#endif
ディレクティブ内で int myVal
を宣言することです。
#define MYVAL 1
#ifdef MYVAL
int myVal;
#endif
L#val
従来のプリプロセッサでは、文字列プレフィックスが文字列化演算子 (#) の結果に誤って結合されます。
#define DEBUG_INFO(val) L"debug prefix:" L#val
// ^
// this prefix
const wchar_t *info = DEBUG_INFO(hello world);
この場合、マクロの展開後に隣接する文字列リテラルが結合されるので、L
プレフィックスは不要です。 下位互換性のある修正は、定義を変更することです。
#define DEBUG_INFO(val) L"debug prefix:" #val
// ^
// no prefix
同じ問題は、引数をワイド文字列リテラルに "文字列化" する便利なマクロでも見つかりました。
// The traditional preprocessor creates a single wide string literal token
#define STRING(str) L#str
この問題は、さまざまな方法で解決できます。
L""
と#str
の文字列の連結を使用して、プレフィックスを追加します。 隣接する文字列リテラルは、マクロ展開後に結合されます。#define STRING1(str) L""#str
追加のマクロ展開で
#str
が文字列化された後、プレフィックスを追加します。#define WIDE(str) L##str #define STRING2(str) WIDE(#str)
連結演算子
##
を使用して、トークンを結合します。##
と#
の操作の順序は指定されていませんが、この場合、すべてのコンパイラは##
の前に#
演算子を評価しているようです。#define STRING3(str) L## #str
無効な ## に関する警告
トークン連結演算子 (##) の結果が単一の有効な前処理トークンにならない場合、その動作は定義されていません。 従来のプリプロセッサでは、トークンの結合が警告なく失敗します。 新しいプリプロセッサは、他のほとんどのコンパイラの動作と一致しており、診断を出力します。
// The ## is unnecessary and does not result in a single preprocessing token.
#define ADD_STD(x) std::##x
// Declare a std::string
ADD_STD(string) s;
可変個引数マクロでのコンマの脱落
従来の MSVC プリプロセッサでは、空の __VA_ARGS__
置換の前に常にコンマが削除されます。 新しいプリプロセッサは、他の一般的なクロスプラットフォーム コンパイラの動作により密接に従います。 コンマを削除するには、可変個引数が (単に空ではなく) 欠落している必要があります。また、それが ##
演算子でマークされている必要があります。 次の例を確認してください。
void func(int, int = 2, int = 3);
// This macro replacement list has a comma followed by __VA_ARGS__
#define FUNC(a, ...) func(a, __VA_ARGS__)
int main()
{
// In the traditional preprocessor, the
// following macro is replaced with:
// func(10,20,30)
FUNC(10, 20, 30);
// A conforming preprocessor replaces the
// following macro with: func(1, ), which
// results in a syntax error.
FUNC(1, );
}
次の例では、FUNC2(1)
の呼び出しで、呼び出されているマクロに可変個引数がありません。 FUNC2(1, )
の呼び出しでは、可変個引数は空ですが、欠落していません (引数リストのコンマに注目してください)。
#define FUNC2(a, ...) func(a , ## __VA_ARGS__)
int main()
{
// Expands to func(1)
FUNC2(1);
// Expands to func(1, )
FUNC2(1, );
}
今後の C++20 標準では、この問題は __VA_OPT__
を追加することで解決されています。 __VA_OPT__
の新しいプリプロセッサ サポートは、Visual Studio 2019 バージョン 16.5 以降で利用できます。
C++20 可変個引数マクロ拡張
新しいプリプロセッサでは、C++20 可変個のマクロ引数の省略がサポートされています。
#define FUNC(a, ...) __VA_ARGS__ + a
int main()
{
int ret = FUNC(0);
return ret;
}
このコードは、C++20 標準より前では準拠していません。 MSVC では、新しいプリプロセッサによってこの C++20 の動作がより低い言語標準のモード (/std:c++14
、/std:c++17
) に拡張されます。 この拡張は、他の主要なクロスプラットフォーム C++ コンパイラの動作と一致します。
マクロ引数は "アンパック" される
従来のプリプロセッサでは、マクロが引数の 1 つを別の依存マクロに転送する場合、挿入時に引数が "アンパック" されません。 通常、この最適化は見過ごされますが、異常な動作につながる可能性があります。
// Create a string out of the first argument, and the rest of the arguments.
#define TWO_STRINGS( first, ... ) #first, #__VA_ARGS__
#define A( ... ) TWO_STRINGS(__VA_ARGS__)
const char* c[2] = { A(1, 2) };
// Conforming preprocessor results:
// const char c[2] = { "1", "2" };
// Traditional preprocessor results, all arguments are in the first string:
// const char c[2] = { "1, 2", };
A()
を展開すると、従来のプリプロセッサは、__VA_ARGS__
にパッケージ化されたすべての引数を TWO_STRINGS の最初の引数に転送します。これにより、TWO_STRINGS
の可変個引数は空のままになります。 これにより、#first
の結果は単なる "1" ではなく "1, 2" になります。 よく理解している場合は、従来のプリプロセッサ拡張で #__VA_ARGS__
の結果がどうなったのか疑問に思われるかもしれません。可変個引数パラメーターが空の場合、文字列リテラル ""
は空になります。 別の問題により、空の文字列リテラル トークンが生成されるのを回避しました。
マクロの置換リストの再スキャン
マクロが置き換えられると、結果のトークンは、置き換えられる追加のマクロ識別子のために再スキャンされます。 実際のコードに基づくこの例に示すように、再スキャンを実行するために従来のプリプロセッサで使用されるアルゴリズムは準拠していません。
#define CAT(a,b) a ## b
#define ECHO(...) __VA_ARGS__
// IMPL1 and IMPL2 are implementation details
#define IMPL1(prefix,value) do_thing_one( prefix, value)
#define IMPL2(prefix,value) do_thing_two( prefix, value)
// MACRO chooses the expansion behavior based on the value passed to macro_switch
#define DO_THING(macro_switch, b) CAT(IMPL, macro_switch) ECHO(( "Hello", b))
DO_THING(1, "World");
// Traditional preprocessor:
// do_thing_one( "Hello", "World");
// Conforming preprocessor:
// IMPL1 ( "Hello","World");
この例は少し不自然に見えるかもしれませんが、実際のコードで見たことがあります。
何が起こっているかを確認するために、DO_THING
で始まる拡張を分解できます。
DO_THING(1, "World")
はCAT(IMPL, 1) ECHO(("Hello", "World"))
に展開されますCAT(IMPL, 1)
はIMPL ## 1
に展開され、これはIMPL1
に展開されます。- これで、トークンは
IMPL1 ECHO(("Hello", "World"))
の状態になります。 - プリプロセッサは、関数のようなマクロ識別子
IMPL1
見つけます。(
が後に続かないため、関数のようなマクロ呼び出しとは見なされません。 - プリプロセッサは、次のトークンに移動します。
ECHO
が呼び出される関数のようなマクロECHO(("Hello", "World"))
を見つけます。これは("Hello", "World")
に展開されます。 IMPL1
は拡張の対象として再び考慮されることはないため、拡張の完全な結果は、IMPL1("Hello", "World");
となります。
新しいプリプロセッサと従来のプリプロセッサの両方で同じように動作するようにマクロを変更するには、間接参照の別のレイヤーを追加します。
#define CAT(a,b) a##b
#define ECHO(...) __VA_ARGS__
// IMPL1 and IMPL2 are macros implementation details
#define IMPL1(prefix,value) do_thing_one( prefix, value)
#define IMPL2(prefix,value) do_thing_two( prefix, value)
#define CALL(macroName, args) macroName args
#define DO_THING_FIXED(a,b) CALL( CAT(IMPL, a), ECHO(( "Hello",b)))
DO_THING_FIXED(1, "World");
// macro expands to:
// do_thing_one( "Hello", "World");
16.5 より前の不完全なフィーチャー
Visual Studio 2019 バージョン 16.5 以降、新しいプリプロセッサは C++20 のフィーチャー コンプリートです。 Visual Studio の以前のバージョンでは、新しいプリプロセッサはほぼ完成していますが、一部のプリプロセッサ ディレクティブ ロジックは従来の動作にフォール バックしています。 16.5 より前のバージョンの Visual Studio の不完全なフィーチャーの一部を次に示します。
_Pragma
のサポート- C++20 のフィーチャー
- ブースト ブロッキングのバグ: プリプロセッサ定数式の論理演算子は、バージョン 16.5 より前の新しいプリプロセッサでは完全には実装されていません。 一部の
#if
ディレクティブでは、新しいプリプロセッサを従来のプリプロセッサにフォール バックできます。 この効果は、従来のプリプロセッサと互換性のないマクロが展開された場合にのみ顕著になります。 これは、Boost プリプロセッサ スロットを構築するときに発生する可能性があります。