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 C++預處理器,以改善標準一致性、修正長期 Bug,以及變更正式未定義的某些行為。 我們也新增了新的診斷,以警告巨集定義中的錯誤。
從 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
您可以透過各種方式修正此問題:
使用 和
#str
的L""
字串串連來新增前置詞。 在巨集擴充之後,相鄰字串常值會結合:#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;
variadic 巨集中的逗號 elision
傳統的 MSVC 預處理器一律會在空 __VA_ARGS__
的取代之前移除逗號。 新的預處理器更緊密地遵循其他熱門跨平臺編譯程序的行為。 若要移除逗號,必須遺漏 variadic 自變數(不只是空白),而且必須以 ##
運算符標示。 請考慮下列範例:
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, );
}
在下列範例中,叫用巨集中遺漏了對 variadic 自變數的呼叫 FUNC2(1)
。 在對 variadic 自變數的 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 variadic 巨集延伸
新的預處理器支援 C++20 variadic 巨集自變數 elision:
#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++編譯程序的行為。
巨集自變數為「已解壓縮」
在傳統的預處理器中,如果巨集將其其中一個自變數轉送至另一個相依巨集,則自變數不會在插入時取得「解壓縮」。 通常不會察覺此優化,但可能會導致異常行為:
// 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 的第一個自變數,這會讓 variadic 自變數保持 TWO_STRINGS
空白。 這會導致的結果 #first
是 “1, 2” ,而不只是 “1”。 如果您緊隨其後,您可能想知道傳統預處理器擴充的結果 #__VA_ARGS__
發生什麼事:如果 variadic 參數是空的,它應該會產生空字串常值 ""
。 另一個問題會讓空字串常值令牌無法產生。
重新掃描巨集的取代清單
取代巨集之後,系統會重新掃描產生的標記,以取得要取代的其他巨集標識碼。 傳統預處理器用來執行重新掃描的演算法不符合規範,如此範例中根據實際程式碼所示:
#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 預處理器位置時,可能會發生此情況。