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:

  1. DO_THING(1, "World") rozbalí na CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) rozbalí na IMPL ## 1položku , která se rozbalí na IMPL1
  3. Teď jsou tokeny v tomto stavu: IMPL1 ECHO(("Hello", "World"))
  4. Preprocesor najde identifikátor IMPL1makra podobný funkci . Vzhledem k tomu, že není za (ním , nepovažuje se za vyvolání makra podobné funkce.
  5. 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")
  6. 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.