Visão geral do novo pré-processador do MSVC

O Visual Studio 2015 usa o pré-processador tradicional, que não está em conformidade com Standard C++ ou C99. Do Visual Studio 2019 versão 16.5 em diante, o novo suporte ao pré-processador para o padrão C++20 está completo. Essas alterações estão disponíveis usando a opção do compilador /Zc:preprocessor. Uma versão experimental do novo pré-processador está disponível no Visual Studio 2017 versão 15.8 e posteriores usando a opção do compilador /experimental:preprocessor. Mais informações sobre como usar o novo pré-processador no Visual Studio 2017 e no Visual Studio 2019 estão disponíveis. Para ver a documentação da sua versão preferencial do Visual Studio, use o controle seletor de Versão. Ele é encontrado na parte superior da tabela de conteúdo nesta página.

Estamos atualizando o pré-processador do Microsoft C++ para melhorar a conformidade com os padrões, corrigir bugs de longa data e alterar alguns comportamentos oficialmente indefinidos. Também adicionamos diagnósticos para avisar sobre erros em definições de macro.

No Visual Studio 2019 versão 16.5 e posteriores, o suporte ao pré-processador para o padrão C++20 tem todos os recursos. Essas alterações estão disponíveis usando a opção do compilador /Zc:preprocessor. Uma versão experimental do novo pré-processador está disponível em versões anteriores do Visual Studio 2017 versão 15.8 em diante. Você pode habilitá-la usando a opção do compilador /experimental:preprocessor. O comportamento padrão do pré-processador permanece igual ao das versões anteriores.

Nova macro predefinida

Você pode detectar qual pré-processador está em uso no tempo de compilação. Verifique o valor da macro _MSVC_TRADITIONAL predefinida para saber se o pré-processador tradicional está em uso. Essa macro é definida de modo incondicional por versões do compilador que dão suporte a ela, independentemente de qual pré-processador é invocado. Seu valor é 1 para o pré-processador tradicional. É 0 para o pré-processador em conformidade.

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

Alterações de comportamento no novo pré-processador

O trabalho inicial no novo pré-processador tem focado fazer com que todas as expansões de macro estejam em conformidade com o padrão. Ele permite que você use o compilador do MSVC com bibliotecas que atualmente são bloqueadas pelos comportamentos tradicionais. Testamos o pré-processador atualizado em projetos do mundo real. Aqui estão algumas das alterações interruptivas mais comuns que encontramos:

Comentários sobre macro

O pré-processador tradicional é baseado em buffers de caracteres, em vez de tokens de pré-processador. Ele permite um comportamento incomum, como o seguinte truque de comentário de pré-processador, que não funciona no pré-processador em conformidade:

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

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

A correção em conformidade com os padrões é declarar int myVal dentro das diretivas #ifdef/#endif apropriadas:

#define MYVAL 1

#ifdef MYVAL
int myVal;
#endif

L#val

O pré-processador tradicional combina incorretamente um prefixo de cadeia de caracteres com o resultado do operador de criação de cadeia de caracteres (#):

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

const wchar_t *info = DEBUG_INFO(hello world);

Nesse caso, o prefixo L é desnecessário porque os literais de cadeia de caracteres adjacentes são combinados após a expansão da macro de qualquer maneira. A correção compatível com versões anteriores é alterar a definição:

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

O mesmo problema é encontrado também em macros de conveniência que "criam uma cadeia de caracteres" do argumento para um literal de cadeia de caracteres largo:

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

Você pode corrigir o problema de várias maneiras:

  • Use a concatenação de cadeia de caracteres de L"" e #str para adicionar o prefixo. Literais de cadeia de caracteres adjacentes são combinados após a expansão da macro:

    #define STRING1(str) L""#str
    
  • Adicionar o prefixo após #str leva à criação de uma cadeia de caracteres com expansão de macro adicional

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • Use o operador de concatenação ## para combinar os tokens. A ordem das operações para ## e # não é especificada, embora todos os compiladores pareçam avaliar o operador # antes de ## neste caso.

    #define STRING3(str) L## #str
    

Aviso sobre ## inválido

Quando o operador de colagem de token (##) não resulta em um só token de pré-processamento válido, o comportamento é indefinido. O pré-processador tradicional falha silenciosamente ao combinar os tokens. O novo pré-processador corresponde ao comportamento da maioria dos outros compiladores e emite um 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;

Elisão de vírgula em macros variádicas

O pré-processador MSVC tradicional sempre remove vírgulas antes de substituições de __VA_ARGS__ vazias. O novo pré-processador acompanha mais de perto o comportamento de outros compiladores multiplataforma populares. Para que a vírgula seja removida, o argumento variádico deve estar ausente (não apenas vazio) e ser marcado com um operador ##. Considere o seguinte exemplo:

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

No exemplo a seguir, na chamada para FUNC2(1), o argumento variádico está ausente na macro que está sendo invocada. Na chamada para FUNC2(1, ), o argumento variádico está vazio, mas não está ausente (observe a vírgula na lista de argumentos).

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

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

No próximo padrão C++20, esse problema foi resolvido adicionando __VA_OPT__. O novo suporte ao pré-processador __VA_OPT__ está disponível no Visual Studio 2019 versão 16.5 e posteriores.

Extensão de macro variádica C++20

O novo pré-processador dá suporte à elisão do argumento de macro variádica C++20:

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

Esse código não está em conformidade antes do padrão C++20. No MSVC, o novo pré-processador estende esse comportamento C++20 para modos padrão de linguagem inferiores (/std:c++14, /std:c++17). Essa extensão corresponde ao comportamento de outros importantes compiladores C++ multiplataforma.

Os argumentos de macro são "desempacotados"

No pré-processador tradicional, se uma macro encaminhar um de seus argumentos para outra macro dependente, o argumento não será "desempacotado" quando for inserido. Normalmente, essa otimização passa despercebida, mas pode levar a um comportamento incomum:

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

Ao expandir A(), o pré-processador tradicional encaminha todos os argumentos empacotados em __VA_ARGS__ para o primeiro argumento de TWO_STRINGS, o que deixa o argumento variádico de TWO_STRINGS vazio. Isso faz com que o resultado de #first seja "1, 2", em vez de apenas "1". Se você estiver acompanhando de perto, pode estar se perguntando o que aconteceu com o resultado de #__VA_ARGS__ na expansão tradicional do pré-processador: se o parâmetro variádico estiver vazio, ele deverá resultar em um literal de cadeia de caracteres vazio "". Um problema separado impediu a geração do token literal de cadeia de caracteres vazio.

Como verificar novamente a lista de substituição de macros

Depois que uma macro é substituída, os tokens resultantes são verificados novamente para que identificadores de macro adicionais sejam substituídos. O algoritmo usado pelo pré-processador tradicional para fazer a nova verificação não está em conformidade, como mostra este exemplo com base no 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");

Embora este exemplo possa parecer um pouco artificial, vimos isso no código do mundo real.

Para ver o que está acontecendo, podemos detalhar a expansão começando com DO_THING:

  1. DO_THING(1, "World") se expande para CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) se expande para IMPL ## 1, que se expande para IMPL1
  3. Agora os tokens estão neste estado: IMPL1 ECHO(("Hello", "World"))
  4. O pré-processador localiza o identificador de macro tipo função IMPL1. Como ele não é seguido por um (, não é considerado uma invocação de macro tipo função.
  5. O pré-processador passa para os tokens a seguir. Ele descobre que a macro tipo função ECHO é invocada: ECHO(("Hello", "World")), que se expande para ("Hello", "World")
  6. IMPL1 nunca mais é considerado para expansão, portanto, o resultado completo das expansões é: IMPL1("Hello", "World");

Para modificar a macro para se comportar da mesma forma no novo pré-processador e no pré-processador tradicional, adicione outra camada de indireção:

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

Recursos incompletos antes da 16.5

No Visual Studio 2019 versão 16.5 e posteriores, o novo pré-processador está completo para o C++20. Nas versões anteriores do Visual Studio, o novo pré-processador está quase completo, embora alguma lógica de diretiva de pré-processador ainda volte ao comportamento tradicional. Aqui está uma lista parcial de recursos incompletos nas versões do Visual Studio antes da 16.5:

  • Compatível com _Pragma
  • Recursos C++20
  • Bug de bloqueio de aumento: operadores lógicos em expressões constantes de pré-processador não são totalmente implementados no novo pré-processador antes da versão 16.5. Em algumas diretivas #if, o novo pré-processador pode voltar ao pré-processador tradicional. O efeito só é perceptível quando macros incompatíveis com o pré-processador tradicional são expandidas. Isso pode acontecer ao criar slots do pré-processador de Aumento.