Enum à la chaîne en C++11

Je me rends compte que a été demandé avant plus d'une fois sur SO mais je n'ai pas pu trouver une question explicitement à la recherche d'une solution actuelle à ce problème avec C++11, alors nous y retournons..

Pouvons-nous facilement obtenir la valeur de chaîne d'une énumération avec C++11?

C'est-à-dire existe-t-il (maintenant) une fonctionnalité intégrée en C++11 qui nous permet d'obtenir une représentation de chaîne des types enum comme dans

typedef enum {Linux, Apple, Windows} OS_type; 
OS_type myOS = Linux;

cout << myOS

Qui imprimerait Linux sur la console?

27
demandé sur Community 2014-01-30 16:14:37

6 réponses

L'absence de longue date et inutile d'une fonctionnalité Générique enum-to-string en C++ (et C) est douloureuse. C++11 n'a pas abordé cela, et pour autant que je sache, C++14 ne le fera pas non plus.

Personnellement, je résoudrais ce problème en utilisant la génération de code. Le préprocesseur C est un moyen-vous pouvez voir d'autres réponses liées dans les commentaires ici pour cela. Mais vraiment, je préfère simplement écrire ma propre génération de code spécifiquement pour les énumérations. Il peut alors générer facilement to_string (char*), from_string, ostream operator<<, istream operator<<, is_valid, et plus de méthodes que de besoin. Cette approche peut être très flexible et puissante, mais elle impose une cohérence absolue entre de nombreuses énumérations Dans un projet, et elle n'entraîne aucun coût d'exécution.

Faites-le en utilisant l'excellent paquet "mako" de Python, ou en Lua si vous êtes dans lightweight, ou le CPP si vous êtes contre les dépendances, ou les propres installations de CMake pour générer du code. Beaucoup de façons, mais tout se résume à la même chose: vous devez générer le code vous-même-C++ ne le fera pas pour vous (malheureusement).

14
répondu John Zwinck 2015-12-18 17:26:42

À mon avis, l'approche la plus maintenable est d'écrire une fonction d'Assistance:

const char* get_name(OS_type os) {
  switch (os) {
  case Linux: return "Linux";
  case Apple: return "Apple";
  case Windows: return "Windows";
  }
}

C'est une bonne idée Pas d'implémenter le cas "par défaut", car cela vous garantira un avertissement du compilateur si vous oubliez d'implémenter un cas (avec les bons paramètres du compilateur et du compilateur).

4
répondu Joel 2015-11-09 00:28:49

J'aime un hack en utilisant le préprocesseur C, que j'ai vu pour la première fois ici: http://blogs.msdn.com/b/vcblog/archive/2008/04/30/enums-macros-unicode-and-token-pasting.aspx .

Il utilise l'opérateur de collage de jetons # .

// This code defines the enumerated values:

#define MY_ENUM(x) x,
enum Fruit_Type {
MY_ENUM(Banana)
MY_ENUM(Apple)
MY_ENUM(Orange)
};
#undef MY_ENUM

// and this code defines an array of string literals for them:

#define MY_ENUM(x) #x,
        const char* const fruit_name[] = {
MY_ENUM(Banana)
MY_ENUM(Apple)
MY_ENUM(Orange)
        };
#undef MY_ENUM

// Finally, here is some client code:

std::cout << fruit_name[Banana] << " is enum #" << Banana << "\n";

// In practice, those three "MY_ENUM" macro calls will be inside an #include file.

Franchement, il est laid et. mais vous finissez par taper vos énumérations exactement une fois dans un fichier include, ce qui est plus maintenable.

BTW, sur ce lien de blog MSDN (voir ci-dessus) un utilisateur a fait un commentaire avec un truc qui rend le tout beaucoup plus joli, et évite #comprend:

#define Fruits(FOO) \
FOO(Apple) \
FOO(Banana) \
FOO(Orange)

#define DO_DESCRIPTION(e)  #e,
#define DO_ENUM(e)  e,

char* FruitDescription[] = {
Fruits(DO_DESCRIPTION)
};

enum Fruit_Type {
Fruits(DO_ENUM)
};

// Client code:

std::cout << FruitDescription[Banana] << " is enum #" << Banana << "\n";

(je viens de remarquer que la réponse de 0x17de utilise également l'opérateur de collage de jetons)

3
répondu AlejoHausner 2015-06-16 16:31:26

Voici un exemple simple utilisant des espaces de noms et des structures. Une classe est créée pour chaque élément enum. Dans cet exemple, j'ai choisi int comme type pour l'id.

#include <iostream>
using namespace std;

#define ENUMITEM(Id, Name) \
struct Name {\
    static constexpr const int id = Id;\
    static constexpr const char* name = #Name;\
};

namespace Food {
ENUMITEM(1, Banana)
ENUMITEM(2, Apple)
ENUMITEM(3, Orange)
}

int main() {
    cout << Food::Orange::id << ":" << Food::Orange::name << endl;
    return 0;
}

Sortie:

3:Orange

== mise à Jour ==

En utilisant:

#define STARTENUM() constexpr const int enumStart = __LINE__;
#define ENUMITEM(Name) \
struct Name {\
    static constexpr const int id = __LINE__ - enumStart - 1;\
    static constexpr const char* name = #Name;\
};

Et en l'utilisant une fois avant la première utilisation de ENUMITEM, les identifiants ne seraient plus nécessaires.

namespace Food {
STARTENUM()
ENUMITEM(Banana)
ENUMITEM(Apple)
ENUMITEM(Orange)
}

La variable enumStart n'est accessible que via l'espace de noms-donc encore plusieurs énumérations peuvent être utilisées.

2
répondu 0x17de 2015-02-04 19:13:19

Vous pouvez utiliser macro pour résoudre ce problème:

#define MAKE_ENUM(name, ...) enum class name { __VA_ARGS__}; \
static std::vector<std::string> Enum_##name##_init(){\
    const std::string content = #__VA_ARGS__; \
    std::vector<std::string> str;\
    size_t len = content.length();\
    std::ostringstream temp;\
    for(size_t i = 0; i < len; i ++) {\
    if(isspace(content[i])) continue;\
    else if(content[i] == ',') {\
    str.push_back(temp.str());\
    temp.str(std::string());}\
    else temp<< content[i];}\
    str.push_back(temp.str());\
    return str;}\
static const std::vector<std::string> Enum_##name##_str_vec = Enum_##name##_init();\
static std::string to_string(name val){\
    return Enum_##name##_str_vec[static_cast<size_t>(val)];\
}\
static std::string print_all_##name##_enum(){\
    int count = 0;\
    std::string ans;\
    for(auto& item:Enum_##name##_str_vec)\
    ans += std::to_string(count++) + ':' + item + '\n';\
    return ans;\
}

Comme la variable statique ne peut être initialisée qu'une seule fois, la fonction Enum_##name##_str_vec utilisera la fonction Enum_##name##_init() pour s'initialiser au début.

L'exemple de code est comme ci-dessous:

MAKE_ENUM(Analysis_Time_Type,
                  UNKNOWN,
                  REAL_TIME, 
                  CLOSSING_TIME 
);

Ensuite, vous pouvez utiliser la phrase ci-dessous pour imprimer une valeur enum:

to_string(Analysis_Time_Type::UNKNOWN)

Et utilisez la phrase ci-dessous pour imprimer toute l'énumération en tant que chaîne:

print_all_Analysis_Time_Type_enum()
0
répondu ynhuang 2017-02-20 09:50:01

Comme mentionné, il n'y a pas de moyen standard de le faire. Mais avec un peu de magie de préprocesseur (similaire à la deuxième contribution D'AlejoHausner) et de la magie de modèle, il peut être assez élégant.

Incluez ce code une fois:

#include <string>
#include <algorithm>

#define ENUM_VALS( name ) name,
#define ENUM_STRINGS( name ) # name,

/** Template function to return the enum value for a given string
*  Note: assumes enums are all upper or all lowercase,
*        that they are contiguous/default-ordered,
*        and that the first value is the default
*  @tparam ENUM     type of the enum to retrieve
*  @tparam ENUMSIZE number of elements in the enum (implicit; need not be passed in)
*  @param valStr   string version of enum value to convert; may be any capitalization (capitalization may be modified)
*  @param enumStrs array of strings corresponding to enum values, assumed to all be in lower/upper case depending upon
*  enumsUpper
*  @param enumsUpper true if the enum values are in all uppercase, false if in all lowercase (mixed case not supported)
*  @return enum value corresponding to valStr, or the first enum value if not found
*/
template <typename ENUM, size_t ENUMSIZE>
static inline ENUM fromString(std::string &valStr, const char *(&enumStrs)[ENUMSIZE], bool enumsUpper = true) {
    ENUM e = static_cast< ENUM >(0); // by default, first value
    // convert valStr to lower/upper-case
    std::transform(valStr.begin(), valStr.end(), valStr.begin(), enumsUpper ? ::toupper : ::tolower);
    for (size_t i = 0; i< ENUMSIZE; i++) {
        if (valStr == std::string(enumStrs[i])) {
            e = static_cast< ENUM >(i);
            break;
        }
    }
    return e;
}

Définissez ensuite chaque énumération comme suit:

//! Define ColorType enum with array for converting to/from strings
#define ColorTypes(ENUM) \
    ENUM(BLACK) \
    ENUM(RED) \
    ENUM(GREEN) \
    ENUM(BLUE)
enum ColorType {
    ColorTypes(ENUM_VALS)
};
static const char* colorTypeNames[] = {
    ColorTypes(ENUM_STRINGS)
};

Il suffit d'énumérer les valeurs enum une seule fois et le code pour le définir est assez compact et intuitif.

Les valeurs seront nécessairement numérotées de la manière par défaut (ie, 0,1,2,...). Le code de fromString() suppose que les valeurs enum sont en majuscules ou en minuscules (pour la conversion à partir de chaînes) que la valeur par défaut est la première, mais vous pouvez bien sûr changer la façon dont ces choses sont gérées.

Voici comment vous obtenez la valeur de chaîne:

ColorType c = ColorType::BLUE;   
std::cout << colorTypeNames[c]; // BLUE

Voici comment vous définissez l'énumération à partir d'une valeur de chaîne:

ColorType c2 = fromString<ColorType>("Green", colorTypeNames); // == ColorType::GREEN
0
répondu jtbr 2017-07-12 12:50:20