Share via


Información general sobre el nuevo preprocesador de MSVC

Visual Studio 2015 usa el preprocesador tradicional, que no se ajusta a los estándares C++ ni C99. A partir de la versión 16.5 de Visual Studio 2019, la compatibilidad del nuevo preprocesador con el estándar C++20 es total. Estos cambios están disponibles mediante el modificador del compilador /Zc:preprocessor. Hay disponible una versión experimental del nuevo preprocesador a partir de la versión 15.8 de Visual Studio 2017 y posteriores mediante el modificador del compilador /experimental:preprocessor. Hay disponible más información sobre el uso del nuevo preprocesador en Visual Studio 2017 y Visual Studio 2019. Para ver la documentación de su versión preferida de Visual Studio, use el control de selector Versión. Se encuentra en la parte superior de la tabla de contenido de esta página.

Se está actualizando el preprocesador de Microsoft C++ para mejorar la conformidad con los estándares, corregir errores antiguos y cambiar algunos comportamientos no definidos oficialmente. También se han agregado nuevos diagnósticos para advertir sobre errores en las definiciones de macros.

A partir de la versión 16.5 de Visual Studio 2019, la compatibilidad del preprocesador con el estándar C++20 es total. Estos cambios están disponibles mediante el modificador del compilador /Zc:preprocessor. Hay disponible una versión experimental del nuevo preprocesador en versiones anteriores a partir de Visual Studio 2017, versión 15.8. Puede habilitarla mediante el modificador del compilador /experimental:preprocessor. El comportamiento predeterminado del preprocesador sigue igual que en versiones anteriores.

Nueva macro predefinida

Puede detectar qué preprocesador está en uso en tiempo de compilación. Compruebe el valor de la macro predefinida _MSVC_TRADITIONAL para saber si el preprocesador tradicional está en uso. Esta macro se establece incondicionalmente mediante las versiones del compilador que la admiten, independientemente de qué preprocesador se invoque. Su valor es 1 en el caso del preprocesador tradicional. Es 0 en el del preprocesador conforme.

#if !defined(_MSVC_TRADITIONAL) || _MSVC_TRADITIONAL
// Logic using the traditional preprocessor
#else
// Logic using cross-platform compatible preprocessor
#endif

Cambios de comportamiento del nuevo preprocesador

El trabajo inicial en el nuevo preprocesador se ha centrado en lograr que todas las expansiones de macros cumplan el estándar. Permite usar el compilador de MSVC con bibliotecas bloqueadas actualmente por los comportamientos tradicionales. El preprocesador actualizado se ha probado en proyectos reales. Estos son algunos de los cambios importantes más comunes detectados:

Comentarios de macros

El preprocesador tradicional se basa en búferes de caracteres en lugar de en tokens de preprocesador. Permite comportamientos inusuales, como el siguiente truco de comentario del preprocesador, que no funciona en el preprocesador conforme:

#if DISAPPEAR
#define DISAPPEARING_TYPE /##/
#else
#define DISAPPEARING_TYPE int
#endif

// myVal disappears when DISAPPEARING_TYPE is turned into a comment
DISAPPEARING_TYPE myVal;

La corrección conforme con los estándares es declarar int myVal dentro de las directivas adecuadas #ifdef/#endif:

#define MYVAL 1

#ifdef MYVAL
int myVal;
#endif

L#val

El preprocesador tradicional combina incorrectamente un prefijo de cadena con el resultado del Operador de conversión a cadenas (#):

#define DEBUG_INFO(val) L"debug prefix:" L#val
//                                       ^
//                                       this prefix

const wchar_t *info = DEBUG_INFO(hello world);

En este caso, el prefijo L no es necesario porque los literales de cadena adyacentes se combinan de todos modos tras la expansión de macros. La corrección compatible con versiones anteriores es cambiar la definición:

#define DEBUG_INFO(val) L"debug prefix:" #val
//                                       ^
//                                       no prefix

También se detecta el mismo problema en macros de conveniencia que "convierten a cadena" el argumento en un literal de cadena ancho:

 // The traditional preprocessor creates a single wide string literal token
#define STRING(str) L#str

Puede solucionar el problema de varias formas:

  • Use la concatenación de cadenas de L"" y #str para agregar un prefijo. Los literales de cadena adyacentes se combinan tras la expansión de macros:

    #define STRING1(str) L""#str
    
  • Agregue el prefijo después de que #str se convierta a cadena con expansión de macros adicional

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • Use el operador de concatenación ## para combinar los tokens. El orden de las operaciones de ## y # no está especificado, aunque todos los compiladores parecen evaluar el operador # antes que ## en este caso.

    #define STRING3(str) L## #str
    

Advertencia sobre ## no válido

Si el Operador de pegado de token (##) no da lugar a un único token de preprocesamiento válido, el comportamiento no está definido. El preprocesador tradicional genera un error silencioso al combinar los tokens. El nuevo preprocesador tiene el comportamiento de la mayoría de los demás compiladores y emite un diagnóstico.

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

Omisión de comas en macros variádicas

El preprocesador de MSVC tradicional siempre quita las comas situadas antes de los reemplazos vacíos __VA_ARGS__. El nuevo preprocesador sigue más estrechamente el comportamiento de otros compiladores multiplataforma populares. Para que se quite la coma, debe faltar el argumento variádico (no solo estar vacío) y debe estar marcado con un operador ##. Considere el ejemplo siguiente:

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, );
}

En el ejemplo siguiente, en la llamada a FUNC2(1), falta el argumento variádico en la macro que se invoca. En la llamada a FUNC2(1, ), el argumento variádico está vacío, pero no falta (observe la coma en la lista de argumentos).

#define FUNC2(a, ...) func(a , ## __VA_ARGS__)
int main()
{
   // Expands to func(1)
   FUNC2(1);

   // Expands to func(1, )
   FUNC2(1, );
}

En el próximo estándar C++20, este problema se ha solucionado al agregar __VA_OPT__. Hay compatibilidad del nuevo preprocesador con __VA_OPT__ a partir de Visual Studio 2019 versión 16.5.

Extensión de macros variádicas de C++20

El nuevo preprocesador admite la omisión de argumentos de macros variádicas de C++20:

#define FUNC(a, ...) __VA_ARGS__ + a
int main()
  {
  int ret = FUNC(0);
  return ret;
  }

Este código no es conforme antes del estándar C++20. En MSVC, el nuevo preprocesador extiende este comportamiento de C++20 a los modos estándar de lenguaje inferior (/std:c++14, /std:c++17). Esta extensión coincide con el comportamiento de otros compiladores de C++ multiplataforma principales.

Los argumentos de macros están "desempaquetados"

En el preprocesador tradicional, si una macro reenvía uno de sus argumentos a otra macro dependiente, el argumento no se "desempaqueta" cuando se inserta. Normalmente, esta optimización pasa desapercibida, pero puede dar lugar a un comportamiento inusual:

// 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", };

Al expandir A(), el preprocesador tradicional reenvía todos los argumentos empaquetados en __VA_ARGS__ al primer argumento de TWO_STRINGS, lo que deja vacío el argumento variádico de TWO_STRINGS. Esto hace que el resultado de #first sea "1, 2", en lugar de solo "1". Si está siguiendo este tema con atención, es posible que se esté preguntando qué ha ocurrido con el resultado de #__VA_ARGS__ en la expansión del preprocesador tradicional: si el parámetro variádico está vacío, debería dar lugar a un literal de cadena vacío "". Un problema independiente hacía que no se generara el token del literal de cadena vacío.

Nuevo examen de la lista de reemplazo de macros

Después de reemplazar una macro, se vuelven a examinar los tokens resultantes para detectar identificadores de macro adicionales que reemplazar. El algoritmo usado por el preprocesador tradicional para realizar el nuevo examen no es conforme, como se muestra en este ejemplo basado en código real:

#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");

Aunque este ejemplo puede parecer un poco artificioso, se ha visto en código real.

Para ver lo que está pasando, se puede desglosar la expansión a partir de DO_THING:

  1. DO_THING(1, "World") se expande a CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) se expande a IMPL ## 1, que se expande a IMPL1
  3. Ahora los tokens están en este estado: IMPL1 ECHO(("Hello", "World"))
  4. El preprocesador encuentra el identificador de macro similar a una función IMPL1. Puesto que no va seguido de (, no se considera una invocación de macro similar a una función.
  5. El preprocesador pasa a los siguientes tokens. Encuentra que se invoca a la macro similar a una función ECHO: ECHO(("Hello", "World")), que se expande a ("Hello", "World")
  6. IMPL1 nunca se vuelve a tener en cuenta para la expansión, por lo que el resultado completo de las expansiones es: IMPL1("Hello", "World");

Para modificar la macro de modo que se comporte de la misma manera en el nuevo preprocesador y en el tradicional, agregue otra capa de direccionamiento indirecto:

#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");

Características incompletas antes de la versión 16.5

A partir de la versión 16.5 de Visual Studio 2019, el nuevo preprocesador tiene todas las características necesarias para C++20. En versiones anteriores de Visual Studio, el nuevo preprocesador está prácticamente completo, aunque alguna lógica de directiva de preprocesador sigue recurriendo al comportamiento tradicional. Esta es una lista parcial de características incompletas en las versiones de Visual Studio anteriores a la versión 16.5:

  • Compatibilidad con _Pragma
  • Características de C++20
  • Error de bloqueo de aumento: los operadores lógicos de expresiones constantes del preprocesador no están completamente implementados en el nuevo preprocesador antes de la versión 16.5. En algunas directivas #if, el nuevo preprocesador puede recurrir al preprocesador tradicional. El efecto solo se nota cuando se expanden las macros que no son compatibles con el preprocesador tradicional. Puede ocurrir al compilar ranuras del preprocesador de aumento.