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 fonctionconstexpr
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 fonctionconstexpr
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 :
Nom | Est statique ? | Est générique ? | Librairie tierce ? | Utilise des macros ? | Est exception-safe? |
Magic Enum | Yes (C++17) | Yes | Yes (Magic Enum) | No | No |
Function w/ exception | Yes (C++14) | No | No | No | No |
Function w/o exception | Yes (C++14) | No | No | No | Yes |
Macro | Yes (C++11) | No | No | Yes | Yes |
Macro & Boost | Yes (C++11) | Yes | Yes (Boost) | Yes | Yes |
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