Partager via


Vue d’ensemble du nouveau préprocesseur MSVC

Visual Studio 2015 utilise le préprocesseur traditionnel, qui n’est pas conforme à standard C++ ou C99. À compter de Visual Studio 2019 version 16.5, la nouvelle prise en charge du préprocesseur pour la norme C++20 est complète. Ces modifications sont disponibles à l’aide du commutateur du compilateur /Zc :preprocesseur . Une version expérimentale du nouveau préprocesseur est disponible à partir de Visual Studio 2017 version 15.8 et ultérieure à l’aide du commutateur du compilateur /experimental :preprocessor . Vous trouverez plus d’informations sur l’utilisation du nouveau préprocesseur dans Visual Studio 2017 et Visual Studio 2019. Pour consulter la documentation sur votre version préférée de Visual Studio, utilisez le contrôle de sélection de Version . Il se trouve en haut de la table des matières de cette page.

Nous mettons à jour le préprocesseur Microsoft C++ pour améliorer la conformité aux normes, corriger les bogues de longue date et modifier certains comportements officiellement non définis. Nous avons également ajouté de nouveaux diagnostics pour avertir les erreurs dans les définitions de macro.

À compter de Visual Studio 2019 version 16.5, la prise en charge du préprocesseur pour la norme C++20 est complète. Ces modifications sont disponibles à l’aide du commutateur du compilateur /Zc :preprocesseur . Une version expérimentale du nouveau préprocesseur est disponible dans les versions antérieures à partir de Visual Studio 2017 version 15.8. Vous pouvez l’activer à l’aide du commutateur du compilateur /experimental :preprocessor . Le comportement de préprocesseur par défaut reste le même que dans les versions précédentes.

Nouvelle macro prédéfinie

Vous pouvez détecter le préprocesseur utilisé au moment de la compilation. Vérifiez la valeur de la macro _MSVC_TRADITIONAL prédéfinie pour savoir si le préprocesseur traditionnel est en cours d’utilisation. Cette macro est définie de manière inconditionnelle par les versions du compilateur qui la prennent en charge, indépendamment de laquelle le préprocesseur est appelé. Sa valeur est 1 pour le préprocesseur traditionnel. C’est 0 pour le préprocesseur conforme.

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

Changements de comportement dans le nouveau préprocesseur

Le travail initial sur le nouveau préprocesseur a été axé sur la mise en conformité de toutes les expansions de macros à la norme. Il vous permet d’utiliser le compilateur MSVC avec des bibliothèques actuellement bloquées par les comportements traditionnels. Nous avons testé le préprocesseur mis à jour sur des projets réels. Voici quelques-uns des changements cassants les plus courants que nous avons trouvés :

Commentaires sur les macros

Le préprocesseur traditionnel est basé sur des mémoires tampons de caractères plutôt que sur des jetons de préprocesseur. Il permet un comportement inhabituel tel que l’astuce de commentaire de préprocesseur suivante, qui ne fonctionne pas sous le préprocesseur 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;

Le correctif conforme aux normes consiste à déclarer int myVal à l’intérieur des directives appropriées #ifdef/#endif :

#define MYVAL 1

#ifdef MYVAL
int myVal;
#endif

L#val

Le préprocesseur traditionnel combine incorrectement un préfixe de chaîne au résultat de l’opérateur stringizing (#) :

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

const wchar_t *info = DEBUG_INFO(hello world);

Dans ce cas, le L préfixe n’est pas nécessaire, car les littéraux de chaîne adjacents sont combinés après l’expansion des macros de toute façon. Le correctif rétrocompatible consiste à modifier la définition :

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

Le même problème se trouve également dans les macros pratiques qui « stringize » l’argument à un littéral de chaîne large :

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

Vous pouvez résoudre le problème de différentes façons :

  • Utilisez la concaténation de chaînes et L""#str pour ajouter un préfixe. Les littéraux de chaîne adjacents sont combinés après l’extension de macro :

    #define STRING1(str) L""#str
    
  • Ajouter le préfixe après #str avoir été stringisé avec une extension de macro supplémentaire

    #define WIDE(str) L##str
    #define STRING2(str) WIDE(#str)
    
  • Utilisez l’opérateur ## de concaténation pour combiner les jetons. L’ordre des opérations pour ## et # n’est pas spécifié, bien que tous les compilateurs semblent évaluer l’opérateur # avant ## dans ce cas.

    #define STRING3(str) L## #str
    

Avertissement sur non valide ##

Lorsque l’opérateur de collage de jetons (##) n’entraîne pas de jeton de prétraitement valide unique, le comportement n’est pas défini. Le préprocesseur traditionnel ne parvient pas à combiner les jetons en mode silencieux. Le nouveau préprocesseur correspond au comportement de la plupart des autres compilateurs et émet un diagnostic.

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

Elision de virgule dans les macros variadiciques

Le préprocesseur MSVC traditionnel supprime toujours les virgules avant les remplacements vides __VA_ARGS__ . Le nouveau préprocesseur suit plus étroitement le comportement d’autres compilateurs multiplateformes populaires. Pour que la virgule soit supprimée, l’argument variadicique doit être manquant (pas seulement vide) et il doit être marqué avec un ## opérateur. Prenons l’exemple suivant :

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

Dans l’exemple suivant, dans l’appel à FUNC2(1) l’argument variadicique est manquant dans la macro appelée. Dans l’appel à FUNC2(1, ) l’argument variadicique est vide, mais pas manquant (notez la virgule dans la liste d’arguments).

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

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

Dans la norme C++20 à venir, ce problème a été résolu en ajoutant __VA_OPT__. La prise en charge du nouveau préprocesseur est __VA_OPT__ disponible à partir de Visual Studio 2019 version 16.5.

Extension de macro variadicique C++20

Le nouveau préprocesseur prend en charge l’elision d’argument de macro variadicique C++20 :

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

Ce code n’est pas conforme avant la norme C++20. Dans MSVC, le nouveau préprocesseur étend ce comportement C++20 aux modes standard de langage inférieurs (/std:c++14, /std:c++17). Cette extension correspond au comportement d’autres compilateurs C++ multiplateformes majeurs.

Les arguments de macro sont « décompressés »

Dans le préprocesseur traditionnel, si une macro transfère l’un de ses arguments à une autre macro dépendante, l’argument n’est pas « décompressé » lorsqu’il est inséré. En règle générale, cette optimisation passe inaperçue, mais elle peut entraîner un comportement inhabituel :

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

Lors de l’expansion A(), le préprocesseur traditionnel transfère tous les arguments empaquetés dans __VA_ARGS__ le premier argument de TWO_STRINGS, ce qui laisse l’argument varidicique de TWO_STRINGS vide. Cela provoque le résultat de #first « 1, 2 » au lieu de « 1 ». Si vous suivez de près, vous vous demandez peut-être ce qui s’est passé au résultat de #__VA_ARGS__ l’expansion du préprocesseur traditionnel : si le paramètre variadicique est vide, il doit entraîner un littéral ""de chaîne vide. Un problème distinct a empêché la génération du jeton littéral de chaîne vide.

Réanalyser la liste de remplacement des macros

Une fois qu’une macro est remplacée, les jetons résultants sont rescannés pour obtenir des identificateurs de macro supplémentaires à remplacer. L’algorithme utilisé par le préprocesseur traditionnel pour effectuer la rescanisation n’est pas conforme, comme illustré dans cet exemple en fonction du code réel :

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

Bien que cet exemple semble un peu contrif, nous l’avons vu dans du code réel.

Pour voir ce qui se passe, nous pouvons décomposer l’expansion en commençant par DO_THING:

  1. DO_THING(1, "World") s’étend sur CAT(IMPL, 1) ECHO(("Hello", "World"))
  2. CAT(IMPL, 1) s’étend sur IMPL ## 1, qui s’étend sur IMPL1
  3. À présent, les jetons sont dans cet état : IMPL1 ECHO(("Hello", "World"))
  4. Le préprocesseur recherche l’identificateur IMPL1de macro de type fonction. Comme il n’est pas suivi d’un (, il n’est pas considéré comme un appel de macro de type fonction.
  5. Le préprocesseur passe aux jetons suivants. Elle recherche la macro ECHO de type fonction appelée : ECHO(("Hello", "World")), qui s’étend sur ("Hello", "World")
  6. IMPL1 n’est jamais considéré à nouveau pour l’expansion, de sorte que le résultat complet des expansions est : IMPL1("Hello", "World");

Pour modifier la macro de la même façon sous le nouveau préprocesseur et le préprocesseur traditionnel, ajoutez une autre couche d’indirection :

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

Fonctionnalités incomplètes antérieures à la version 16.5

À compter de Visual Studio 2019 version 16.5, le nouveau préprocesseur est complet pour C++20. Dans les versions précédentes de Visual Studio, le nouveau préprocesseur est principalement terminé, même si une logique de directive de préprocesseur revient toujours au comportement traditionnel. Voici une liste partielle des fonctionnalités incomplètes dans les versions de Visual Studio antérieures à la version 16.5 :

  • Prise en charge de _Pragma
  • Fonctionnalités C++20
  • Amélioration du bogue bloquant : les opérateurs logiques dans les expressions constantes de préprocesseur ne sont pas entièrement implémentés dans le nouveau préprocesseur avant la version 16.5. Sur certaines #if directives, le nouveau préprocesseur peut revenir au préprocesseur traditionnel. L’effet est visible uniquement lorsque les macros incompatibles avec le préprocesseur traditionnel sont développées. Cela peut se produire lors de la création d’emplacements de préprocesseur Boost.