Übersicht über den neuen MSVC-Präprozessor

Visual Studio 2015 verwendet den herkömmlichen Präprozessor, der nicht mit Standard C++ oder C99 übereinstimmt. Ab Visual Studio 2019, Version 16.5, ist die neue Preprozessorunterstützung für den C++20-Standard featurevervollständigen. Diese Änderungen stehen mithilfe des Compilerschalters "/Zc:preprocessor " zur Verfügung. Eine experimentelle Version des neuen Präprozessors ist ab Visual Studio 2017, Version 15.8 und höher, mit der Compileroption "/experimental:preprocessor " verfügbar. Weitere Informationen zur Verwendung des neuen Präprozessors in Visual Studio 2017 und Visual Studio 2019 sind verfügbar. Um die Dokumentation für Ihre bevorzugte Version von Visual Studio anzuzeigen, verwenden Sie das Auswahlsteuerelement Version. Es befindet sich am Anfang des Inhaltsverzeichnisses auf dieser Seite.

Wir aktualisieren den Microsoft C++-Präprozessor, um die Standardskonformität zu verbessern, lange bestehende Fehler zu beheben und einige Verhaltensweisen zu ändern, die offiziell nicht definiert sind. Außerdem haben wir neue Diagnose hinzugefügt, um Fehler in Makrodefinitionen zu warnen.

Ab Visual Studio 2019, Version 16.5, ist die Preprozessorunterstützung für den C++20-Standard featurevervollständigen. Diese Änderungen stehen mithilfe des Compilerschalters "/Zc:preprocessor " zur Verfügung. Eine experimentelle Version des neuen Präprozessors ist ab Visual Studio 2017, Version 15.8, in früheren Versionen verfügbar. Sie können sie mithilfe des Compilerschalters "/experimental:preprocessor " aktivieren. Das Standardverhalten des Vorprozessors Standard entspricht in früheren Versionen.

Neues vordefiniertes Makro

Sie können erkennen, welcher Präprozessor zur Kompilierungszeit verwendet wird. Überprüfen Sie den Wert des vordefinierten Makros _MSVC_TRADITIONAL , um festzustellen, ob der herkömmliche Präprozessor verwendet wird. Dieses Makro wird bedingungslos durch Versionen des Compilers festgelegt, die es unterstützen, unabhängig davon, welche Präprozessor aufgerufen wird. Der Wert ist 1 für den herkömmlichen Präprozessor. Es ist 0 für den konformen Präprozessor.

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

Verhaltensänderungen im neuen Präprozessor

Die anfängliche Arbeit an dem neuen Präprozessor wurde darauf ausgerichtet, alle Makroerweiterungen dem Standard entsprechen zu lassen. Damit können Sie den MSVC-Compiler mit Bibliotheken verwenden, die derzeit von den herkömmlichen Verhaltensweisen blockiert werden. Wir haben den aktualisierten Präprozessor auf realen Projekten getestet. Hier sind einige der häufiger auftretenden Änderungen, die wir gefunden haben:

Makrokommentare

Der herkömmliche Präprozessor basiert auf Zeichenpuffern und nicht auf Präprozessortoken. Es ermöglicht ungewöhnliches Verhalten wie den folgenden Vorprozessorkommentar-Trick, der nicht unter dem konformen Präprozessor funktioniert:

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

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

Der Standardkonforme Fix besteht darin, innerhalb der entsprechenden #ifdef/#endif Direktiven zu deklarierenint myVal:

#define MYVAL 1

#ifdef MYVAL
int myVal;
#endif

L#val

Der herkömmliche Präprozessor kombiniert fälschlicherweise ein Zeichenfolgenpräfix mit dem Ergebnis des Zeichenfolgenoperators (#):

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

const wchar_t *info = DEBUG_INFO(hello world);

In diesem Fall ist das L Präfix unnötig, da die angrenzenden Zeichenfolgenliterale trotzdem nach der Makroerweiterung kombiniert werden. Der abwärtskompatible Fix besteht darin, die Definition zu ändern:

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

Das gleiche Problem wird auch in Komfortmakros gefunden, die das Argument in ein breites Zeichenfolgenliteral "stringisieren":

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

Sie können das Problem auf verschiedene Arten beheben:

  • Verwenden Sie die Zeichenfolgenverkettung und L""#str fügen Sie präfix hinzu. Benachbarte Zeichenfolgenliterale werden nach der Makroerweiterung kombiniert:

    #define STRING1(str) L""#str
    
  • Hinzufügen des Präfixes nach #str der Zeichenfolge mit zusätzlicher Makroerweiterung

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • Verwenden Sie den Verkettungsoperator ## , um die Token zu kombinieren. Die Reihenfolge der Vorgänge für ## und # ist nicht angegeben, obwohl alle Compiler den # Operator in ## diesem Fall auswerten scheinen.

    #define STRING3(str) L## #str
    

Warnung für ungültig ##

Wenn der Token-Einfügen-Operator (##) kein einzelnes gültiges Präverarbeitungstoken zur Folge hat, ist das Verhalten nicht definiert. Der herkömmliche Präprozessor kann die Token nicht automatisch kombinieren. Der neue Präprozessor entspricht dem Verhalten der meisten anderen Compiler und gibt eine Diagnose aus.

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

Kommas elision in variadischen Makros

Der herkömmliche MSVC-Präprozessor entfernt immer Kommas vor leeren __VA_ARGS__ Ersetzungen. Der neue Präprozessor folgt genauer dem Verhalten anderer beliebter plattformübergreifender Compiler. Damit das Komma entfernt werden kann, muss das variadische Argument fehlen (nicht nur leer), und es muss mit einem ## Operator gekennzeichnet werden. Betrachten Sie das folgende Beispiel:

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

Im folgenden Beispiel fehlt im Aufruf FUNC2(1) des variadischen Arguments das makro, das aufgerufen wird. Im Aufruf des FUNC2(1, ) variadischen Arguments ist leer, aber nicht vorhanden (beachten Sie das Komma in der Argumentliste).

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

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

Im bevorstehenden C++20-Standard wurde dieses Problem durch Hinzufügen __VA_OPT__behoben. Ab Visual Studio 2019, Version 16.5, ist die neue Präprozessorunterstützung __VA_OPT__ verfügbar.

C++20 variadische Makroerweiterung

Der neue Präprozessor unterstützt C++20 variadische Makroargumente elision:The new preprocessor supports C++20 variadic macro argument elision:

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

Dieser Code entspricht nicht vor dem C++20-Standard. In MSVC erweitert der neue Präprozessor dieses C++20-Verhalten auf Modi mit niedrigerer Sprache (/std:c++14, /std:c++17). Diese Erweiterung entspricht dem Verhalten anderer plattformübergreifender C++-Compiler.

Makroargumente sind "entpackt"

Wenn ein Makro im herkömmlichen Präprozessor eines seiner Argumente an ein anderes abhängiges Makro weiterleitet, wird das Argument beim Einfügen nicht "entpackt" angezeigt. In der Regel wird diese Optimierung unbemerkt, kann aber zu einem ungewöhnlichen Verhalten führen:

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

Beim Erweitern A()leitet der herkömmliche Präprozessor alle Argumente weiter, die in __VA_ARGS__ das erste Argument von TWO_STRINGS verpackt sind, wodurch das variadische Argument leer TWO_STRINGS bleibt. Das führt dazu, dass das Ergebnis #first "1, 2" und nicht nur "1" ist. Wenn Sie genau folgen, fragen Sie sich vielleicht, was mit dem Ergebnis der #__VA_ARGS__ herkömmlichen Präprozessorerweiterung passiert ist: Wenn der variadische Parameter leer ist, sollte es zu einem leeren Zeichenfolgenliteral ""führen. Ein separates Problem hat das leere Zeichenfolgenliteraltoken nicht generiert.

Neuscannen der Ersetzungsliste für Makros

Nachdem ein Makro ersetzt wurde, werden die resultierenden Token erneut überprüft, um zusätzliche Makro-IDs zu ersetzen. Der algorithmus, der vom herkömmlichen Präprozessor zum Ausführen des Rescans verwendet wird, entspricht nicht, wie in diesem Beispiel basierend auf dem tatsächlichen Code gezeigt:

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

Obwohl dieses Beispiel ein bisschen konriviert erscheinen mag, haben wir es im realen Code gesehen.

Um zu sehen, was los ist, können wir die Erweiterung abbrechen, beginnend mit DO_THING:

  1. DO_THING(1, "World") erweitert auf CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) erweitert auf IMPL ## 1, die erweitert wird IMPL1
  3. Jetzt befinden sich die Token in diesem Zustand: IMPL1 ECHO(("Hello", "World"))
  4. Der Präprozessor findet den funktionsähnlichen Makrobezeichner IMPL1. Da es nicht von einem (folgt, wird es nicht als funktionsähnlicher Makroaufruf betrachtet.
  5. Der Präprozessor wechselt zu den folgenden Token. Es wird das funktionsähnliche Makro ECHO aufgerufen: ECHO(("Hello", "World")), das erweitert wird ("Hello", "World")
  6. IMPL1 wird nie wieder als Erweiterung betrachtet, so dass das vollständige Ergebnis der Expansionen: IMPL1("Hello", "World");

Um das Makro so zu ändern, dass es sich sowohl unter dem neuen Präprozessor als auch dem herkömmlichen Präprozessor auf die gleiche Weise verhält, fügen Sie eine weitere Dereferenzierungsebene hinzu:

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

Unvollständige Features vor 16.5

Ab Visual Studio 2019, Version 16.5, ist der neue Präprozessor featurevervollständigen für C++20. In früheren Versionen von Visual Studio ist der neue Präprozessor hauptsächlich vollständig, obwohl einige Präprozessordirektive logik immer noch auf das herkömmliche Verhalten zurückfällt. Nachfolgend finden Sie eine Teilliste unvollständiger Features in Visual Studio-Versionen vor 16.5:

  • Unterstützung für _Pragma
  • C++20-Features
  • Fehler beim Blockieren der Verstärkung: Logische Operatoren in Präprozessorkonstantenausdrücken werden vor Version 16.5 nicht vollständig im neuen Präprozessor implementiert. Bei einigen #if Direktiven kann der neue Präprozessor auf den herkömmlichen Präprozessor zurückgreifen. Der Effekt ist nur dann spürbar, wenn Makros, die nicht mit dem herkömmlichen Präprozessor kompatibel sind, erweitert werden. Es kann passieren, wenn Boost-Präprozessorplätze erstellt werden.