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
:
DO_THING(1, "World")
se expande paraCAT(IMPL, 1) ECHO(("Hello", "World"))
CAT(IMPL, 1)
se expande paraIMPL ## 1
, que se expande paraIMPL1
- Agora os tokens estão neste estado:
IMPL1 ECHO(("Hello", "World"))
- 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. - 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")
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.