Přehled nového preprocesoru MSVC
Visual Studio 2015 používá tradiční preprocesor, který nevyhovuje standardu C++ nebo C99. Počínaje sadou Visual Studio 2019 verze 16.5 je nová podpora preprocesoru pro standard C++20 dokončená. Tyto změny jsou k dispozici pomocí přepínače kompilátoru /Zc:preprocessor . Experimentální verze nového preprocesoru je k dispozici od sady Visual Studio 2017 verze 15.8 a novější pomocí přepínače kompilátoru /experimental:preprocessor . Další informace o použití nového preprocesoru v sadě Visual Studio 2017 a Visual Studio 2019 jsou k dispozici. Pokud chcete zobrazit dokumentaci pro upřednostňovanou verzi sady Visual Studio, použijte ovládací prvek selektoru verzí . Nachází se v horní části obsahu na této stránce.
Aktualizujeme preprocesor jazyka Microsoft C++, abychom zlepšili shodu standardů, opravili dlouhodobé chyby a změnili některá chování, která jsou oficiálně nedefinovaná. Přidali jsme také novou diagnostiku, která upozorňuje na chyby v definicích maker.
Počínaje sadou Visual Studio 2019 verze 16.5 je podpora preprocesoru pro standard C++20 dokončena. Tyto změny jsou k dispozici pomocí přepínače kompilátoru /Zc:preprocessor . Experimentální verze nového preprocesoru je dostupná ve starších verzích počínaje sadou Visual Studio 2017 verze 15.8. Můžete ho povolit pomocí přepínače kompilátoru /experimental:preprocessor . Výchozí chování preprocesoru zůstává stejné jako v předchozích verzích.
Nové předdefinované makro
Můžete zjistit, který preprocesor se používá v době kompilace. Zkontrolujte hodnotu předdefinovaného makra _MSVC_TRADITIONAL
a zjistěte, jestli se používá tradiční preprocesor. Toto makro je bezpodmínečně nastaveno verzemi kompilátoru, které ho podporují, nezávisle na tom, který preprocesor je vyvolán. Jeho hodnota je 1 pro tradiční preprocesor. Je to 0 pro odpovídající preprocesor.
#if !defined(_MSVC_TRADITIONAL) || _MSVC_TRADITIONAL
// Logic using the traditional preprocessor
#else
// Logic using cross-platform compatible preprocessor
#endif
Změny chování v novém preprocesoru
Počáteční práce na novém preprocesoru byla zaměřena na to, aby všechna rozšíření maker odpovídala standardu. Umožňuje používat kompilátor MSVC s knihovnami, které jsou aktuálně blokovány tradičním chováním. Aktualizovaný preprocesor jsme otestovali na skutečných projektech. Tady jsou některé z nejběžnějších zásadních změn, které jsme našli:
Komentáře k makrem
Tradiční preprocesor je založený na vyrovnávacích pamětích znaků, nikoli na tokenech preprocesoru. Umožňuje neobvyklé chování, jako je například následující trik s komentářem preprocesoru, který nefunguje pod odpovídajícím preprocesorem:
#if DISAPPEAR
#define DISAPPEARING_TYPE /##/
#else
#define DISAPPEARING_TYPE int
#endif
// myVal disappears when DISAPPEARING_TYPE is turned into a comment
DISAPPEARING_TYPE myVal;
Oprava vyhovující standardům je deklarovat int myVal
uvnitř odpovídajících #ifdef/#endif
direktiv:
#define MYVAL 1
#ifdef MYVAL
int myVal;
#endif
L#val
Tradiční preprocesor nesprávně kombinuje předponu řetězce s výsledkem operátoru stringizing (#):
#define DEBUG_INFO(val) L"debug prefix:" L#val
// ^
// this prefix
const wchar_t *info = DEBUG_INFO(hello world);
V tomto případě je předpona nepotřebná, L
protože sousední řetězcové literály jsou přesto sloučeny po rozšíření makra. Zpětně kompatibilní oprava spočívá ve změně definice:
#define DEBUG_INFO(val) L"debug prefix:" #val
// ^
// no prefix
Stejný problém najdete také v makrech pohodlí, která argument "stringize" na široký řetězcový literál:
// The traditional preprocessor creates a single wide string literal token
#define STRING(str) L#str
Tento problém můžete vyřešit různými způsoby:
Použijte zřetězení
L""
řetězců a#str
přidejte předponu. Sousední řetězcové literály se kombinují po rozšíření makra:#define STRING1(str) L""#str
Přidání předpony za
#str
řetězcem s dalším rozšířením makra#define WIDE(str) L##str #define STRING2(str) WIDE(#str)
Ke kombinování tokenů použijte operátor
##
zřetězení. Pořadí operací pro##
a#
není zadáno, i když se zdá, že všechny kompilátory vyhodnocují#
operátor dříve##
v tomto případě.#define STRING3(str) L## #str
Upozornění na neplatné ##
Pokud operátor vkládání tokenů (##) nemá za následek jeden platný token předběžného zpracování, chování není definováno. Tradiční preprocesor bezobslužně nezkombinuje tokeny. Nový preprocesor odpovídá chování většiny ostatních kompilátorů a generuje diagnostiku.
// 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;
Čárka elize variadických makrech
Tradiční preprocesor MSVC vždy odebere čárky před prázdnými __VA_ARGS__
nahrazeními. Nový preprocesor přesněji sleduje chování dalších oblíbených kompilátorů pro různé platformy. Aby se čárka odebrala, musí argument variadic chybět (nejen prázdný) a musí být označen operátorem ##
. Představte si následující příklad:
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, );
}
V následujícím příkladu ve volání FUNC2(1)
argumentu variadic chybí v vyvolání makra. FUNC2(1, )
Volání argumentu variadic je prázdné, ale chybí (všimněte si čárky v seznamu argumentů).
#define FUNC2(a, ...) func(a , ## __VA_ARGS__)
int main()
{
// Expands to func(1)
FUNC2(1);
// Expands to func(1, )
FUNC2(1, );
}
V nadcházejícím standardu C++20 byl tento problém vyřešen přidáním __VA_OPT__
. Od verze Visual Studio 2019 verze 16.5 je k dispozici nová podpora __VA_OPT__
preprocesoru.
Rozšíření makra C++20 variadic
Nový preprocesor podporuje elizi argumentu makra C++20 variadic:
#define FUNC(a, ...) __VA_ARGS__ + a
int main()
{
int ret = FUNC(0);
return ret;
}
Tento kód neodpovídá před standardem C++20. V MSVC nový preprocesor rozšiřuje toto chování C++20 na režimy nižšího jazyka Standard (/std:c++14
, /std:c++17
). Toto rozšíření odpovídá chování jiných hlavních kompilátorů jazyka C++ pro různé platformy.
Argumenty makra jsou "rozbalené".
Pokud makro v tradičním preprocesoru předá jeden z jeho argumentů jinému závislému makru, nebude se při vložení argumentu "rozbalit". Tato optimalizace obvykle není povšimnutá, ale může vést k neobvyklému chování:
// 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", };
Při rozbalování předá A()
tradiční preprocesor všechny argumenty zabalené __VA_ARGS__
do prvního argumentu TWO_STRINGS, který ponechá variadický argument TWO_STRINGS
prázdný. To způsobí, že výsledek #first
bude "1, 2" místo jen "1". Pokud sledujete pozorně, možná vás zajímá, co se stalo s výsledkem tradičního #__VA_ARGS__
rozšíření preprocesoru: pokud je parametr variadic prázdný, měl by výsledkem prázdný řetězcový literál ""
. Samostatný problém ponechal vygenerování prázdného řetězcového literálového tokenu.
Opětovné prohledání náhradního seznamu pro makra
Po nahrazení makra se výsledné tokeny znovu naskenují, aby se nahradily další identifikátory maker. Algoritmus používaný tradičním preprocesorem pro opětovné prohledání neodpovídá, jak je znázorněno v tomto příkladu na základě skutečného kódu:
#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");
I když se tento příklad může zdát trochu nepochybný, viděli jsme ho v reálném kódu.
Abychom viděli, co se děje, můžeme rozčlenit rozšíření počínaje DO_THING
:
DO_THING(1, "World")
rozbalí naCAT(IMPL, 1) ECHO(("Hello", "World"))
CAT(IMPL, 1)
rozbalí naIMPL ## 1
položku , která se rozbalí naIMPL1
- Teď jsou tokeny v tomto stavu:
IMPL1 ECHO(("Hello", "World"))
- Preprocesor najde identifikátor
IMPL1
makra podobný funkci . Vzhledem k tomu, že není za(
ním , nepovažuje se za vyvolání makra podobné funkce. - Preprocesor se přesune na následující tokeny. Najde vyvolání makra
ECHO
podobného funkci:ECHO(("Hello", "World"))
, která se rozbalí na("Hello", "World")
IMPL1
se nikdy znovu nepovažuje za rozšíření, takže úplný výsledek rozšíření je:IMPL1("Hello", "World");
Chcete-li upravit makro tak, aby se chovalo stejným způsobem jako u nového preprocesoru i tradičního preprocesoru, přidejte další vrstvu nepřímého zpracování:
#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");
Neúplné funkce před 16.5
Počínaje sadou Visual Studio 2019 verze 16.5 je nový preprocesor dokončený pro C++20. V předchozích verzích sady Visual Studio je nový preprocesor většinou dokončený, i když se logika direktivy preprocesoru stále vrací k tradičnímu chování. Tady je částečný seznam neúplných funkcí ve verzích sady Visual Studio před verzí 16.5:
- Podpora pro
_Pragma
- Funkce C++20
- Zvýšení chyby blokování: Logické operátory v konstantních výrazech preprocesoru nejsou plně implementovány v novém preprocesoru před verzí 16.5. U některých
#if
direktiv se nový preprocesor může vrátit k tradičnímu preprocesoru. Efekt je patrný pouze v případech, kdy se rozbalí makra nekompatibilní s tradičním preprocesorem. Může k tomu dojít při vytváření slotů preprocesoru Boost.