Share via


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 預處理器位置時,可能會發生此情況。