MSVC nový preprocesor – přehled

Visual Studio 2015 používá tradiční preprocesor, který není v souladu se standardem C++ nebo C99. 2019 Visual Studio počínaje verzí 16,5 verze je nová podpora preprocesoru pro standard c++ 20 kompletní funkcí. Tyto změny jsou k dispozici pomocí přepínače kompilátoru /Zc: preprocesor . experimentální verze nového preprocesoru je k dispozici od verze Visual Studio 2017 15,8 nebo novější pomocí přepínače kompilátoru /experimental: preprocesor . další informace o použití nového preprocesoru v Visual Studio 2017 a Visual Studio 2019 je k dispozici. chcete-li zobrazit dokumentaci k preferované verzi Visual Studio, použijte ovládací prvek selektor verzí . Nachází se v horní části obsahu na této stránce.

Aktualizujeme preprocesor Microsoft C++ pro zlepšení dodržování standardů, opravu chyb dlouhodobě zavazuje chránit a změnu některých chování, která jsou oficiálně nedefinovaná. Přidali jsme také novou diagnostiku, která upozorňuje na chyby v definicích maker.

od verze Visual Studio 2019 16,5 je podpora preprocesoru pro standard c++ 20 funkce dokončena. Tyto změny jsou k dispozici pomocí přepínače kompilátoru /Zc: preprocesor . experimentální verze nového preprocesoru je k dispozici v dřívějších verzích od verze Visual Studio 2017 15,8. Můžete ji povolit pomocí přepínače kompilátoru /Experimental: preprocesor . 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. Ověřte hodnotu předdefinovaného makra _MSVC_TRADITIONAL a sdělte, zda se tradiční preprocesor používá. Toto makro je nastaveno na nepodmíněné verze kompilátoru, který ho podporuje, nezávisle na tom, který preprocesor je vyvolán. Jeho hodnota je 1 pro tradiční preprocesor. Je 0 pro vyhovují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 se zaměřuje na to, že všechna rozšíření makra jsou v souladu se standardem. umožňuje použít MSVC kompilátor s knihovnami, které jsou aktuálně blokovány tradičním chováním. Otestovali jsme aktualizované preprocesory na projektech reálného světa. Tady jsou některé z nejběžnějších nejnovějších změn, které jsme našli:

Komentáře makra

Tradiční preprocesor je založen na vyrovnávací paměti znaků spíše než tokeny preprocesoru. Umožňuje neobvyklé chování, jako je například následující štych komentáře preprocesoru, který nefunguje s vyhovují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 deklarována int myVal v příslušných #ifdef/#endif direktivách:

#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 operátoru převodu (#) :

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

const wchar_t *info = DEBUG_INFO(hello world);

V tomto případě L je předpona zbytečná, protože sousední řetězcové literály jsou kombinovány po rozšíření makra. Zpětně kompatibilní oprava je změna definice:

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

Stejný problém je také nalezen v praktických makrech, které "převádějícím" argumentem na řetězec literálu v šířce:

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

Problém můžete vyřešit různými způsoby:

  • Použijte zřetězení řetězců L"" a #str k přidání předpony. Sousední řetězcové literály jsou kombinovány po rozšíření makra:

    #define STRING1(str) L""#str
    
  • Přidání předpony po #str řetězce s dalšími rozšířeními makra

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • ##K kombinování tokenů použijte operátor zřetězení. Pořadí operací pro ## a # je neurčeno, i když všechny kompilátory se jeví k vyhodnocení # operátoru před ## v tomto případě.

    #define STRING3(str) L## #str
    

Upozornění na neplatné ##

Pokud operátor vložení tokenu (# #) nevede k jednomu platnému tokenu předzpracování, chování není definováno. Tradiční preprocesor bez upozornění nemůže kombinovat tokeny. Nový preprocesor se shoduje s chováním 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 Elizi v makrech variadické

tradiční preprocesor MSVC vždy odebere čárky před prázdné __VA_ARGS__ náhrady. Nový preprocesor se podrobněji řídí chováním ostatních oblíbených kompilátorů pro různé platformy. Aby byla čárka odebrána, argument variadické musí chybět (není to nic prázdné) a musí být označený pomocí ## operátoru. Uvažujte 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 variadické chybí ve vyvolaném makru. V volání FUNC2(1, ) argumentu variadické 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 řešen přidáním __VA_OPT__ . nová podpora preprocesoru pro __VA_OPT__ je dostupná od verze Visual Studio 2019 16,5.

Rozšíření makra c++ 20 variadické

Nový preprocesor podporuje Argument makra C++ 20 variadické Elizi:

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

Tento kód se neshoduje s standardem C++ 20. v MSVC nový preprocesor rozšiřuje toto chování jazyka c++ 20 na nižší standardní režimy ( /std:c++14 , /std:c++17 ). Toto rozšíření se shoduje s chováním dalších hlavních kompilátorů C++ pro různé platformy.

Argumenty makra jsou "unpacked".

V tradičním preprocesoru, pokud makro předává některý z jeho argumentů jinému závislému makru, pak argument nezíská "unpacked" při vložení. Obvykle se tato optimalizace neprojeví, 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 rozšiřování A() tradiční preprocesor předá všechny argumenty v rámci __VA_ARGS__ do prvního argumentu TWO_STRINGS, což ponechá argument variadické TWO_STRINGS prázdný. To způsobí, že výsledkem #first bude "1, 2" místo pouze "1". Pokud vás podržíte úzce, může vás zajímat, co se stalo s výsledkem #__VA_ARGS__ v tradičním rozšíření preprocesoru: Pokud je parametr variadické prázdný, měla by být výsledkem prázdný řetězcový literál "" . Samostatný problém zachová prázdný řetězcový literálový token od vygenerování.

Znovu prohledat náhradní seznam pro makra

Po nahrazení makra se u výsledných tokenů znovu vyhledají další identifikátory maker, které se mají nahradit. Algoritmus používaný tradičním preprocesorem pro provedení opětovného prohledání není v souladu s tímto příkladem, jak je znázorněno v tomto příkladu na základě aktuální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 bit contrived, zobrazili jsme ho v reálném světě.

Pokud chcete zjistit, co se právě používá, můžeme rozšířit rozšíření od DO_THING :

  1. DO_THING(1, "World") rozšíří na CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) rozšíří na IMPL ## 1 , který se rozšíří na IMPL1
  3. Teď jsou tokeny v tomto stavu: IMPL1 ECHO(("Hello", "World"))
  4. Preprocesor vyhledá identifikátor makra podobného funkci IMPL1 . Vzhledem k tomu, že není následován ( , není považován za vyvolání makra jako funkce.
  5. Preprocesor se přesune na následující tokeny. Nalezne vyvolané makro podobné funkci ECHO : ECHO(("Hello", "World")) , které se rozšíří na ("Hello", "World")
  6. IMPL1 se nikdy 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 v rámci nového preprocesoru i tradičního preprocesoru, přidejte další vrstvu dereference:

#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

od verze Visual Studio 2019 16,5 je nový preprocesor funkcí dokončený pro c++ 20. v předchozích verzích Visual Studio je nový preprocesor většinou kompletní, i když se některá z těchto direktiv preprocesoru stále nevrátí k tradičnímu chování. tady je částečný seznam neúplných funkcí v Visual Studio verzích před 16,5:

  • Podpora pro _Pragma
  • Funkce c++ 20
  • Zesílení blokující chybu: 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 může nový preprocesor vrátit k tradičnímu preprocesoru. Tento efekt je patrné pouze v případě, že jsou makra nekompatibilní s tradičním preprocesorem rozbalena. K tomu může dojít při sestavování, když se budou zvyšovat sloty preprocesoru.