Panoramica del nuovo preprocessore MSVC

Visual Studio 2015 usa il preprocessore tradizionale, che non è conforme allo standard C++ o C99. A partire da Visual Studio 2019 versione 16.5, il nuovo supporto del preprocessore per lo standard C++20 è completo delle funzionalità. Queste modifiche sono disponibili usando l'opzione del compilatore /Zc:preprocessore . Una versione sperimentale del nuovo preprocessore è disponibile a partire da Visual Studio 2017 versione 15.8 e successive usando l'opzione del compilatore /experimental:preprocessor . Altre informazioni sull'uso del nuovo preprocessore in Visual Studio 2017 e Visual Studio 2019 sono disponibili. Per visualizzare la documentazione relativa alla versione preferita di Visual Studio, usare il controllo selettore della versione . Si trova nella parte superiore del sommario in questa pagina.

Il preprocessore Microsoft C++ viene aggiornato per migliorare la conformità agli standard, correggere bug di lunga durata e modificare alcuni comportamenti ufficialmente non definiti. È stata aggiunta anche una nuova diagnostica per avvisare gli errori nelle definizioni di macro.

A partire da Visual Studio 2019 versione 16.5, il supporto del preprocessore per lo standard C++20 è completo delle funzionalità. Queste modifiche sono disponibili usando l'opzione del compilatore /Zc:preprocessore . Una versione sperimentale del nuovo preprocessore è disponibile nelle versioni precedenti a partire da Visual Studio 2017 versione 15.8. È possibile abilitarla usando l'opzione del compilatore /experimental:preprocessor . Il comportamento predefinito del preprocessore rimane identico a quello delle versioni precedenti.

Nuova macro predefinita

È possibile rilevare il preprocessore in uso in fase di compilazione. Controllare il valore della macro _MSVC_TRADITIONAL predefinita per indicare se il preprocessore tradizionale è in uso. Questa macro viene impostata in modo incondizionato dalle versioni del compilatore che lo supportano, indipendentemente dal preprocessore richiamato. Il valore è 1 per il preprocessore tradizionale. È 0 per il preprocessore conforme.

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

Modifiche del comportamento nel nuovo preprocessore

Il lavoro iniziale sul nuovo preprocessore è stato incentrato sulla conformità di tutte le espansioni di macro allo standard. Consente di usare il compilatore MSVC con librerie attualmente bloccate dai comportamenti tradizionali. Il preprocessore aggiornato è stato testato su progetti reali. Ecco alcune delle modifiche più comuni che causano un'interruzione:

Commenti macro

Il preprocessore tradizionale si basa su buffer di caratteri anziché token di preprocessore. Consente comportamenti insoliti, ad esempio il seguente trucco di commento del preprocessore, che non funziona nel preprocessore conforme:

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

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

La correzione conforme agli standard consiste nel dichiarare int myVal all'interno delle direttive appropriate #ifdef/#endif :

#define MYVAL 1

#ifdef MYVAL
int myVal;
#endif

L#val

Il preprocessore tradizionale combina erroneamente un prefisso stringa al risultato dell'operatore di stringizzazione (#):

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

const wchar_t *info = DEBUG_INFO(hello world);

In questo caso, il L prefisso non è necessario perché i valori letterali stringa adiacenti vengono combinati comunque dopo l'espansione della macro. La correzione compatibile con le versioni precedenti consiste nel modificare la definizione:

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

Lo stesso problema si trova anche nelle macro utili che "stringi" l'argomento in un valore letterale stringa wide:

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

È possibile risolvere il problema in diversi modi:

  • Usare la concatenazione di stringhe di L"" e #str per aggiungere il prefisso. I valori letterali stringa adiacenti vengono combinati dopo l'espansione della macro:

    #define STRING1(str) L""#str
    
  • Aggiungere il prefisso dopo #str la stringa con l'espansione di macro aggiuntiva

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • Usare l'operatore ## di concatenazione per combinare i token. L'ordine delle operazioni per ## e # non è specificato, anche se tutti i compilatori sembrano valutare l'operatore # prima ## in questo caso.

    #define STRING3(str) L## #str
    

Avviso in caso di errore non valido ##

Quando l'operatore token-incolla (##) non comporta un singolo token di pre-elaborazione valido, il comportamento non è definito. Il preprocessore tradizionale non riesce a combinare automaticamente i token. Il nuovo preprocessore corrisponde al comportamento della maggior parte degli altri compilatori e genera una diagnostica.

// 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;

Elisione da virgole nelle macro variadic

Il preprocessore MSVC tradizionale rimuove sempre le virgole prima delle sostituzioni vuote __VA_ARGS__ . Il nuovo preprocessore segue più da vicino il comportamento di altri compilatori multipiattaforma più diffusi. Per rimuovere la virgola, l'argomento variadic deve essere mancante (non solo vuoto) e deve essere contrassegnato con un ## operatore. Si consideri l'esempio seguente:

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

Nell'esempio seguente, nella chiamata all'argomento FUNC2(1) variadic manca nella macro da richiamare. Nella chiamata all'argomento FUNC2(1, ) variadic è vuoto, ma non manca (si noti la virgola nell'elenco di argomenti).

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

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

Nel prossimo standard C++20 questo problema è stato risolto aggiungendo __VA_OPT__. Il nuovo supporto del preprocessore per __VA_OPT__ è disponibile a partire da Visual Studio 2019 versione 16.5.

Estensione di macro variadic C++20

Il nuovo preprocessore supporta l'elisione dell'argomento macro variadic C++20:

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

Questo codice non è conforme prima dello standard C++20. In MSVC il nuovo preprocessore estende questo comportamento C++20 alle modalità standard del linguaggio inferiori (/std:c++14, /std:c++17). Questa estensione corrisponde al comportamento di altri principali compilatori C++ multipiattaforma.

Gli argomenti macro sono "decompressi"

Nel preprocessore tradizionale, se una macro inoltra uno dei relativi argomenti a un'altra macro dipendente, l'argomento non viene decompresso quando viene inserito. In genere questa ottimizzazione non viene rilevata, ma può causare comportamenti insoliti:

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

Quando si espande A(), il preprocessore tradizionale inoltra tutti gli argomenti inseriti nel __VA_ARGS__ pacchetto al primo argomento di TWO_STRINGS, lasciando vuoto l'argomento variadic.TWO_STRINGS Ciò fa sì che il risultato di #first sia "1, 2" anziché solo "1". Se si segue da vicino, è possibile chiedersi cosa è successo al risultato dell'espansione #__VA_ARGS__ tradizionale del preprocessore: se il parametro variadic è vuoto, dovrebbe comportare un valore letterale ""stringa vuoto. Un problema separato impediva la generazione del token letterale stringa vuoto.

Riesecuzione dell'elenco di sostituzione per le macro

Dopo la sostituzione di una macro, i token risultanti vengono riprovati per individuare altri identificatori di macro da sostituire. L'algoritmo usato dal preprocessore tradizionale per eseguire la nuova analisi non è conforme, come illustrato in questo esempio in base al codice effettivo:

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

Anche se questo esempio può sembrare un po ' contrived, è stato visto nel codice reale.

Per vedere cosa sta succedendo, è possibile suddividere l'espansione a partire da DO_THING:

  1. DO_THING(1, "World") si espande in CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) si espande in IMPL ## 1, che si espande in IMPL1
  3. I token sono ora in questo stato: IMPL1 ECHO(("Hello", "World"))
  4. Il preprocessore trova l'identificatore IMPL1di macro simile alla funzione . Poiché non è seguito da , (non è considerato una chiamata di macro simile a una funzione.
  5. Il preprocessore passa ai token seguenti. Trova che viene richiamata la macro simile a una funzione ECHO : ECHO(("Hello", "World")), che si espande in ("Hello", "World")
  6. IMPL1 non è mai considerato di nuovo per l'espansione, quindi il risultato completo delle espansioni è: IMPL1("Hello", "World");

Per modificare la macro in modo che si comporti allo stesso modo sia nel nuovo preprocessore che nel preprocessore tradizionale, aggiungere un altro livello di riferimento indiretto:

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

Funzionalità incomplete prima della 16.5

A partire da Visual Studio 2019 versione 16.5, il nuovo preprocessore è completo per C++20. Nelle versioni precedenti di Visual Studio, il nuovo preprocessore è per lo più completo, anche se alcune logiche di direttiva del preprocessore continuano a rientrare nel comportamento tradizionale. Ecco un elenco parziale delle funzionalità incomplete nelle versioni di Visual Studio precedenti alla 16.5:

  • Supporto per _Pragma
  • Funzionalità di C++20
  • Bug di blocco boost: gli operatori logici nelle espressioni costanti del preprocessore non sono completamente implementati nel nuovo preprocessore prima della versione 16.5. In alcune #if direttive, il nuovo preprocessore può eseguire il fallback al preprocessore tradizionale. L'effetto è evidente solo quando le macro non sono compatibili con il preprocessore tradizionale vengono espanse. Può verificarsi quando si compilano slot di preprocessore Boost.