Обзор нового препроцессора 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++ для улучшения соответствия стандартам, исправления давних ошибок и изменения некоторых действий, которые официально не определены. Мы также добавили новые диагностика, чтобы предупредить об ошибках в определениях макросов.

Начиная с 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;

Исправление соответствия стандартам заключается в объявлении int myVal внутри соответствующих #ifdef/#endif директив:

#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

Эта же проблема также найдена в удобных макросах, которые "stringize" аргумент в широкий строковый литерал:

 // 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;

Удаление запятых в макросах variadic

Традиционный препроцессор 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, );
}

В следующем примере в вызове FUNC2(1) аргумента variadic отсутствует в вызываемом макросе. В вызове FUNC2(1, ) аргумента variadic пусто, но не отсутствует (обратите внимание на запятую в списке аргументов).

#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:

#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, что оставляет пустый аргумент вариационного аргумента 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. Препроцессор переходит к следующим токенам. Он находит макрос ECHO , подобный функции, вызывается: ECHO(("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 новый препроцессор в основном завершен, хотя некоторые логики директивы препроцессора по-прежнему возвращаются к традиционному поведению. Ниже приведен частичный список неполных функций в версиях Visual Studio до 16.5:

  • Поддержка _Pragma
  • Функции C++20
  • Ошибка блокировки повышения. Логические операторы в константных выражениях препроцессора не полностью реализованы в новом препроцессоре до версии 16.5. В некоторых #if директивах новый препроцессор может вернуться к традиционному препроцессору. Эффект заметно только при том, что макросы несовместимы с традиционным препроцессором. Это может произойти при создании слотов препроцессора boost.