分享方式:


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

您可以透過各種方式修正此問題:

  • 使用 和 #strL""字串串連來新增前置詞。 相鄰字串常值會在展開巨集之後合併:

    #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分解擴充:

  1. DO_THING(1, "World") 展開至 CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) 展開至 IMPL ## 1,其擴充至 IMPL1
  3. 令牌現在處於此狀態: IMPL1 ECHO(("Hello", "World"))
  4. 預處理器會尋找類似函式巨集識別碼 IMPL1。 因為它後面沒有 (,所以它不會被視為類似函式的巨集調用。
  5. 預處理器會移至下列令牌。 它會尋找叫用類似函式的巨集 ECHOECHO(("Hello", "World")),這會展開至 ("Hello", "World")
  6. 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 預處理器位置時,可能會發生此情況。