Différentes manières pour convertir un enum en string

Article original : Best ways to convert an enum to a string | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Une problématique particulièrement récurrente que les développeurs C++ rencontrent (tous niveaux confondus) est comment afficher la valeur d’un enum de manière claire et lisible.

Le problème est qu’il n’y a pas une seule solution objectivement meilleure que les autres. Cela dépend beaucoup des contraintes liées à l’environnement, les besoins spécifiques et, comme toujours, la version de C++ utilisée.

Cet article compile une petite liste de manière d’ajouter la réflexivité aux enums.

NB : si vous connaissez une autre manière de faire qui a son propre intérêt, n’hésitez pas à la partager en commentaire.

Bien sûr, idéalement on voudrait que cette réflexivité soit statique. Dans la plupart des cas listés ci-dessous, on aura recours au mot-clé constexpr.

La librairie Magic Enum

Magic Enum est une librairie qui fait exactement ce qu’on cherche à faire : elle ajoute de moyens de donner la réflexivité aux enums.

Avec elle, vous pouvez convertir statiquement en chaîne de caractère (et accessoirement depuis une chaîne de caractère) des enums que vous avez définis. En gros, elle implémente l’enum_cast. Elle permet aussi d’itérer sur la liste des littéraux d’un enum.

Vous pourrez la trouver ici : GitHub – Neargye/magic_enum: Static reflection for enums (to string, from string, iteration) for modern C++, work with any enum type without any macro or boilerplate code

Inconvénients

  • C’est une librairie tierce.
  • Ne fonctionne qu’en C++17
  • Vous avez besoin d’une version spécifique de votre compilateur pour qu’elle fonctionne (Clang >= 5, MSVC >= 15.3 et GCC >= 9)
  • Elle a d’autres contraintes liées à son fonctionnement interne (consultez la page Limitations de sa documentation pour plus d’informations : magic_enum/limitations.md at master · Neargye/magic_enum · GitHub)

Utiliser une fonction dédiée (avec exception)

Version statique

constexpr est un mot-clé fabuleux qui nous permet de définir des expressions de manière statiques. Quand on l’utilise dans le type de retour d’une fonction, cela permet au compilateur d’évaluer statiquement le résultat de cette fonction.

Dans cette version, j’ai placé exception dans le default du switch-case pour s’assurer que si on ajoute un littéral mais qu’on oublie de mettre à jour la fonction, l’exception soit levée pour nous prévenir.

#include <iostream>
 
enum class Esper { Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek };
 
constexpr const char* EsperToString(Esper e) throw()
{
    switch (e)
    {
        case Esper::Unu: return "Unu";
        case Esper::Du: return "Du";
        case Esper::Tri: return "Tri";
        case Esper::Kvar: return "Kvar";
        case Esper::Kvin: return "Kvin";
        case Esper::Ses: return "Ses";
        case Esper::Sep: return "Sep";
        case Esper::Ok: return "Ok";
        case Esper::Naux: return "Naux";
        case Esper::Dek: return "Dek";
        default: throw std::invalid_argument("Unimplemented item");
    }
}
 
int main()
{
    std::cout << EsperToString(Esper::Kvin) << std::endl;
}

Version dynamique

Le problème est qu’avoir plusieurs return dans une fonction constexpr est une fonctionnalité disponible à partir du C++14. Si vous utilisez une version antérieure, vous devez retirer le constexpr et rendre la fonction dynamique pour qu’elle compile :

#include <iostream>
 
enum class Esper { Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek };
 
const char* EsperToString(Esper e) throw()
{
    switch (e)
    {
        case Esper::Unu: return "Unu";
        case Esper::Du: return "Du";
        case Esper::Tri: return "Tri";
        case Esper::Kvar: return "Kvar";
        case Esper::Kvin: return "Kvin";
        case Esper::Ses: return "Ses";
        case Esper::Sep: return "Sep";
        case Esper::Ok: return "Ok";
        case Esper::Naux: return "Naux";
        case Esper::Dek: return "Dek";
        default: throw std::invalid_argument("Unimplemented item");
    }
}
 
int main()
{
    std::cout << EsperToString(Esper::Kvin) << std::endl;
}

Si vous utilisez une version antérieure aux C++11, vous pouvez remplacer l’enum class par un simple enum pour que cela fonctionne.

Inconvénients

  • Avoir plusieurs return dans une fonction constexpr est C++14 (pour la version statique)
  • Il faut écrire une fonction par enum est ladite fonction est très verbeuse.
  • Lève une exception

Utiliser une fonction dédiée sans exception)

Version statique

Parfois, on préfère un code qui ne peut pas lever d’exception, quoi qu’il arrive. Ou peut-être êtes-vous comme moi et compilez votre code avec -Werror. Dans les deux cas, vous pouvez écrire la même fonction que dans l’exemple précédent, sans lever d’exception.

Vous avez juste à surveiller les warnings quand vous ajoutez des éléments à votre enum.

#include <iostream>
 
enum class Esper { Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek };
 
constexpr const char* EsperToString(Esper e) noexcept
{
    switch (e)
    {
        case Esper::Unu: return "Unu";
        case Esper::Du: return "Du";
        case Esper::Tri: return "Tri";
        case Esper::Kvar: return "Kvar";
        case Esper::Kvin: return "Kvin";
        case Esper::Ses: return "Ses";
        case Esper::Sep: return "Sep";
        case Esper::Ok: return "Ok";
        case Esper::Naux: return "Naux";
        case Esper::Dek: return "Dek";
    }
}
 
int main()
{
    std::cout << EsperToString(Esper::Kvin) << std::endl;
}

Version dynamique

Une fois de plus, la version dynamique sans constexpr :

#include <iostream>
 
enum class Esper { Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek };
 
const char* EsperToString(Esper e) noexcept
{
    switch (e)
    {
        case Esper::Unu: return "Unu";
        case Esper::Du: return "Du";
        case Esper::Tri: return "Tri";
        case Esper::Kvar: return "Kvar";
        case Esper::Kvin: return "Kvin";
        case Esper::Ses: return "Ses";
        case Esper::Sep: return "Sep";
        case Esper::Ok: return "Ok";
        case Esper::Naux: return "Naux";
        case Esper::Dek: return "Dek";
    }
}
 
int main()
{
    std::cout << EsperToString(Esper::Kvin) << std::endl;
}

Inconvénients

  • Avoir plusieurs return dans une fonction constexpr est C++14 (pour la version statique)
  • Il faut écrire une fonction par enum est ladite fonction est très verbeuse.
  • Il y a un risque de ne pas voir les warnings quand un élément est ajouté sans que la fonction soit mise à jour.

Avec une macro

Les macros peuvent faire beaucoup de choses que le code dynamique ne peut pas faire. Voici deux implémentations de réflexivité d’enum avec une macro.

Version statique

#include <iostream>
 
#define ENUM_MACRO(name, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)\
    enum class name { v1, v2, v3, v4, v5, v6, v7, v8, v9, v10 };\
    const char *name##Strings[] = { #v1, #v2, #v3, #v4, #v5, #v6, #v7, #v8, #v9, #v10};\
    template<typename T>\
    constexpr const char *name##ToString(T value) { return name##Strings[static_cast<int>(value)]; }
 
ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek);
 
int main()
{
    std::cout << EsperToString(Esper::Kvin) << std::endl;
}

Version dynamique

Très similaire à la version statique, mais celle-ci est compatible avec du code antérieur au C++11 (constexpr et enum class sont des mots-clés apparus en C++11) :

#include <iostream>
 
#define ENUM_MACRO(name, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10)\
    enum name { v1, v2, v3, v4, v5, v6, v7, v8, v9, v10 };\
    const char *name##Strings[] = { #v1, #v2, #v3, #v4, #v5, #v6, #v7, #v8, #v9, #v10 };\
    const char *name##ToString(int value) { return name##Strings[value]; }
 
ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek);
 
int main()
{
    std::cout << EsperToString(Kvin) << std::endl;
}

Inconvénients

  • Utilise une macro (je pourrais écrire un article parlant des macros et de leur toxicité au sein d’une codebase — j’en ai déjà un peu parlé et le referai, mais ce n’est pas le sujet ici et maintenant. Cependant, gardez à l’esprit que si vous ne savez pourquoi les macros peuvent être dangereuses, alors vous ne devriez pas les utiliser).
  • Vous avez besoin d’écrire une macro différente chaque fois que vous avez besoin d’un enum avec un nombre d’éléments différents (et elle devra avoir un nom différent, ce qui est pénible).

Avec une macro et Boost

On peut contourner l’inconvénient du « nombre d’éléments dans l’enum » en utilisant des fonctionnalités de Boost.

Version statique

#include <iostream>
#include <boost/preprocessor.hpp>
 
#define PROCESS_ONE_ELEMENT(r, unused, idx, elem) \
  BOOST_PP_COMMA_IF(idx) BOOST_PP_STRINGIZE(elem)
 
#define ENUM_MACRO(name, ...)\
    enum class name { __VA_ARGS__ };\
    const char *name##Strings[] = { BOOST_PP_SEQ_FOR_EACH_I(PROCESS_ONE_ELEMENT, %%, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) };\
    template<typename T>\
    constexpr const char *name##ToString(T value) { return name##Strings[static_cast<int>(value)]; }
 
ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek);
 
int main()
{
    std::cout << EsperToString(Esper::Kvin) << std::endl;
}

Dans cette version (un peu plus complexe que les précédentes), PROCESS_ONE_ELEMENT « convertit » l’élément en une version « stringifiée » (à l’aide de BOOST_PP_STRINGIZE) et BOOST_PP_SEQ_FOR_EACH_I permet d’itérer sur tous les éléments de __VA_ARGS__ (qui est le pack de paramètres de la macro).

Version dynamique

Une version quasi-identique, mais une fois encore sans constexpr et sans enum class pour être compatible pré-C++11 :

#include <iostream>
#include <boost/preprocessor.hpp>
 
#define PROCESS_ONE_ELEMENT(r, unused, idx, elem) \
  BOOST_PP_COMMA_IF(idx) BOOST_PP_STRINGIZE(elem)
 
#define ENUM_MACRO(name, ...)\
    enum name { __VA_ARGS__ };\
    const char *name##Strings[] = { BOOST_PP_SEQ_FOR_EACH_I(PROCESS_ONE_ELEMENT, %%, BOOST_PP_VARIADIC_TO_SEQ(__VA_ARGS__)) };\
    const char *name##ToString(int value) { return name##Strings[value]; }
 
ENUM_MACRO(Esper, Unu, Du, Tri, Kvar, Kvin, Ses, Sep, Ok, Naux, Dek);
 
int main()
{
    std::cout << EsperToString(Kvin) << std::endl;
}

Inconvénients

  • Utilise une macro (…)
  • Utilise Boost

NB: bien que ce soit techniquement une librairie tierce, Boost est plus facilement intégrable sur un projet que les librairies plus méconnue comme Magic Enum.

En résumé

Voici un tableau résumant les avantages et inconvénients de chaque méthode :

NomEst statique ?Est générique ?Librairie tierce ?Utilise des macros ?Est exception-safe?
Magic EnumYes (C++17)YesYes (Magic Enum)NoNo
Function w/ exceptionYes (C++14)NoNoNoNo
Function w/o exception Yes (C++14)NoNoNoYes
MacroYes (C++11)NoNoYesYes
Macro & BoostYes (C++11)YesYes (Boost)YesYes

Je le répète : si vous connaissez une méthode intéressante de convertir un enum en string, partagez-la en commentaire !

Merci de votre attention et à la semaine prochaine !

Article original : Best ways to convert an enum to a string | Belay the C++ (belaycpp.com)
Traductrice : Chloé Lourseyre

Laisser un commentaire