web-dev-qa-db-fra.com

Quel typeums Enum en C++ utilisez-vous?

Tout le monde sait que les énumérations intégrées à C++ ne sont pas sécurisées. Je me demandais quelles classes implémentant des enums typesafe sont utilisées ... J'utilise moi-même le "vélo" suivant, mais il est quelque peu verbeux et limité:

typesafeenum.h:

struct TypesafeEnum
{
// Construction:
public:
    TypesafeEnum(): id (next_id++), name("") {}
    TypesafeEnum(const std::string& n): id(next_id++), name(n) {}

// Operations:
public:
    bool operator == (const TypesafeEnum& right) const;
    bool operator != (const TypesafeEnum& right) const;
    bool operator < (const TypesafeEnum& right) const;

    std::string to_string() const { return name; }

// Implementation:
private:
    static int next_id;
    int id;
    std::string name;
};

typesafeenum.cpp:

int TypesafeEnum::next_id = 1;

bool TypesafeEnum::operator== (const TypesafeEnum& right) const 
{ return id == right.id; }

bool TypesafeEnum::operator!= (const TypesafeEnum& right) const 
{ return !operator== (right); }

bool TypesafeEnum::operator< (const TypesafeEnum& right) const  
{ return id < right.id; }

Usage:

class Dialog 
{
 ...
    struct Result: public TypesafeEnum
    {
        static const Result CANCEL("Cancel");
        static const Result OK("Ok");
    };


    Result doModal();
 ...
};

const Dialog::Result Dialog::Result::OK;
const Dialog::Result Dialog::Result::CANCEL;

Ajout: Je pense que j'aurais dû être plus précis sur les exigences. Je vais essayer de les résumer:

Priorité 1: Définir une variable enum sur une valeur non valide devrait être impossible (une erreur de compilation) sans exception.

Priorité 2: La conversion d'une valeur enum en/à partir d'un int devrait être possible avec un seul appel explicite de fonction/méthode.

Priorité 3: Déclaration et utilisation aussi compactes, élégantes et pratiques que possible

Priorité 4: Conversion des valeurs enum en et à partir de chaînes.

Priorité 5: (C'est bien agréable) Possibilité de parcourir les valeurs enum.

44
Alex Jenter

Je suis en train de jouer avec la proposition Boost.Enum de Boost Vault (nom_fichier enum_rev4.6.Zip). Bien qu'il n'ait jamais été officiellement soumis pour inclusion dans Boost, il est utilisable tel quel. (La documentation fait défaut mais est compensée par un code source clair et de bons tests.)

Boost.Enum vous permet de déclarer une énumération comme celle-ci:

BOOST_ENUM_VALUES(Level, const char*,
    (Abort)("unrecoverable problem")
    (Error)("recoverable problem")
    (Alert)("unexpected behavior")
    (Info) ("expected behavior")
    (Trace)("normal flow of execution")
    (Debug)("detailed object state listings")
)

Et faites-le s'étendre automatiquement à ceci:

class Level : public boost::detail::enum_base<Level, string>
{
public:
    enum domain
    {
        Abort,
        Error,
        Alert,
        Info,
        Trace,
        Debug,
    };

    BOOST_STATIC_CONSTANT(index_type, size = 6);

    Level() {}
    Level(domain index) : boost::detail::enum_base<Level, string>(index) {}

    typedef boost::optional<Level> optional;
    static optional get_by_name(const char* str)
    {
        if(strcmp(str, "Abort") == 0) return optional(Abort);
        if(strcmp(str, "Error") == 0) return optional(Error);
        if(strcmp(str, "Alert") == 0) return optional(Alert);
        if(strcmp(str, "Info") == 0) return optional(Info);
        if(strcmp(str, "Trace") == 0) return optional(Trace);
        if(strcmp(str, "Debug") == 0) return optional(Debug);
        return optional();
    }

private:
    friend class boost::detail::enum_base<Level, string>;
    static const char* names(domain index)
    {
        switch(index)
        {
        case Abort: return "Abort";
        case Error: return "Error";
        case Alert: return "Alert";
        case Info: return "Info";
        case Trace: return "Trace";
        case Debug: return "Debug";
        default: return NULL;
        }
    }

    typedef boost::optional<value_type> optional_value;
    static optional_value values(domain index)
    {
        switch(index)
        {
        case Abort: return optional_value("unrecoverable problem");
        case Error: return optional_value("recoverable problem");
        case Alert: return optional_value("unexpected behavior");
        case Info: return optional_value("expected behavior");
        case Trace: return optional_value("normal flow of execution");
        case Debug: return optional_value("detailed object state listings");
        default: return optional_value();
        }
    }
};

Cela répond aux cinq priorités que vous avez énumérées.

42
Josh Kelley

Une méthode de compromis intéressante est la suivante:

struct Flintstones {
   enum E {
      Fred,
      Barney,
      Wilma
   };
};

Flintstones::E fred = Flintstones::Fred;
Flintstones::E barney = Flintstones::Barney;

Votre version n'est pas typée sécurité, mais son utilisation est meilleure que celle des énumérations standard, et vous pouvez toujours tirer parti de la conversion d'entier lorsque vous en avez besoin.

19
Charlie

J'utilise C++ 0x typesafe enums . J'utilise des modèles/macros d'aide qui fournissent la fonctionnalité chaîne à/de.

enum class Result { Ok, Cancel};
12
Roddy

Je ne. Beaucoup trop de frais généraux pour peu d'avantages. De plus, être capable de classer des énumérations dans différents types de données pour la sérialisation est un outil très pratique. Je n'ai jamais vu un cas où une énumération "Type safe" mériterait la surcharge et la complexité où C++ offre déjà une mise en oeuvre suffisamment bonne.

6
nlaq

Personnellement, j'utilise une version adaptée du typesafe enum idiom . Il ne fournit pas les cinq "exigences" que vous avez énoncées dans votre modification, mais je suis tout à fait en désaccord avec certaines d'entre elles. Par exemple, je ne vois pas comment Prio # 4 (conversion de valeurs en chaînes) a quelque chose à voir avec le type safety. La plupart des représentations temporelles de valeurs individuelles doivent toujours être séparées de la définition du type (pensez à i18n pour une raison simple). Le Prio n ° 5 (iteratio, qui est facultatif) est l’une des choses les plus agréables que je voudrais voir naturellement se passe dans les enums, je me suis donc senti triste que cela apparaisse comme "facultatif" dans votre requête, mais semble mieux être adressé via un système d'itération séparé tel que beginname __/endfunction ou un enum_iterator, ce qui les rend compatibles avec STL et C++ 11 foreach.

OTOH, cet idiome simple fournit joliment Prio # 3 Prio # 1 grâce au fait qu’il enveloppe uniquement enumname__s avec plus d’informations sur le type. Sans compter que c'est une solution très simple qui, pour la plupart, ne nécessite aucun en-tête de dépendance externe, il est donc assez facile à transporter. Il a aussi l'avantage de faire des énumérations étendues à la-C++ 11:

// This doesn't compile, and if it did it wouldn't work anyway
enum colors { salmon, .... };
enum fishes { salmon, .... };

// This, however, works seamlessly.
struct colors_def { enum type { salmon, .... }; };
struct fishes_def { enum type { salmon, .... }; };

typedef typesafe_enum<colors_def> colors;
typedef typesafe_enum<fishes_def> fishes;

Le seul "trou" que cette solution fournit est qu'elle ne résout pas le fait qu'elle n'empêche pas enumname__s de types différents (ou un enumet un int) d'être comparés directement, car lorsque vous utilisez des valeurs directement, vous forcez les valeurs implicites. conversion en intname__:

if (colors::salmon == fishes::salmon) { .../* Ooops! */... }

Mais jusqu'à présent, j'ai trouvé que de tels problèmes peuvent être résolus en offrant simplement une meilleure comparaison avec le compilateur - par exemple, en fournissant explicitement un opérateur qui compare deux types enumdifférents, puis en forçant son échec:

// I'm using backports of C++11 utilities like static_assert and enable_if
template <typename Enum1, typename Enum2>
typename enable_if< (is_enum<Enum1>::value && is_enum<Enum2>::value) && (false == is_same<Enum1,Enum2>::value) , bool >
::type operator== (Enum1, Enum2) {
    static_assert (false, "Comparing enumerations of different types!");
}

Bien que cela ne semble pas avoir réussi à casser le code jusqu’à présent, et que ce soit pour traiter explicitement le problème spécifique sans faire autre chose, je ne suis pas sûr que ce soit une chose " should " do ( Je soupçonne que cela va interférer avec enumname__s participant déjà à des opérateurs de conversion déclarés ailleurs; je serais ravi de recevoir des commentaires à ce sujet). 

En combinant cela avec l'idiome de typesafe ci-dessus, vous obtenez quelque chose d'assez proche de C++ 11 enum class en humanibilité (lisibilité et maintenabilité) sans rien faire de trop obscur. Et je dois admettre que c'était amusant à faire, je n'avais jamais pensé à réellement ask le compilateur si j'avais affaire à enumname__s ou pas ...

2
Luis Machuca

Mon point de vue est que vous inventez un problème et que vous y apportez une solution. Je ne vois pas la nécessité de créer un cadre élaboré pour une énumération de valeurs. Si vous êtes dédié à faire en sorte que vos valeurs soient uniquement membres d'un certain ensemble, vous pouvez pirater une variante d'un type de données unique.

2
Paul Nathan

J'ai donné une réponse à ceci ici , sur un sujet différent. C'est un style d'approche différent qui permet la plupart des mêmes fonctionnalités sans nécessiter de modification de la définition de l'énumération d'origine (et donc, permettant l'utilisation dans les cas où vous ne définissez pas l'énum). Il permet également de vérifier les plages d'exécution.

L'inconvénient de mon approche est qu'elle n'impose pas de manière programmée le couplage entre la classe énumérée et la classe auxiliaire, elles doivent donc être mises à jour en parallèle. Cela fonctionne pour moi, mais YMMV.

1
Nick

Je pense que Java enum serait un bon modèle à suivre. En gros, le formulaire Java ressemblerait à ceci:

public enum Result {
    OK("OK"), CANCEL("Cancel");

    private final String name;

    Result(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

L’intérêt de l’approche Java est que OK et CANCEL sont des instances uniques et immuables de Result (avec les méthodes que vous voyez). Vous ne pouvez créer aucune autre instance de Result. Comme ce sont des singletons, vous pouvez comparer par pointeur/référence - très pratique. :-)

ETA: En Java, au lieu de faire des masques de bits à la main, vous utilisez plutôt une variable EnumSet pour spécifier un jeu de bits (elle implémente l'interface Set et fonctionne comme des ensembles --- mais implémentée avec des masques de bits). Beaucoup plus lisible que la manipulation de bitmask écrite à la main!

1
Chris Jester-Young

Utilisez boost::variant!

Après avoir essayé beaucoup des idées ci-dessus et les avoir trouvées manquantes, j'ai choisi cette approche simple:

#include <iostream>
#include <boost/variant.hpp>

struct A_t {};
static const A_t A = A_t();
template <typename T>
bool isA(const T & x) { if(boost::get<A_t>(&x)) return true; return false; }

struct B_t {};
static const B_t B = B_t();
template <typename T>
bool isB(const T & x) { if(boost::get<B_t>(&x)) return true; return false; }

struct C_t {};
static const C_t C = C_t();
template <typename T>
bool isC(const T & x) { if(boost::get<C_t>(&x)) return true; return false; }

typedef boost::variant<A_t, B_t> AB;
typedef boost::variant<B_t, C_t> BC;

void ab(const AB & e)
{
  if(isA(e))
    std::cerr << "A!" << std::endl;
  if(isB(e))
    std::cerr << "B!" << std::endl;
  // ERROR:
  // if(isC(e))
  //   std::cerr << "C!" << std::endl;

  // ERROR:
  // if(e == 0)
  //   std::cerr << "B!" << std::endl;
}

void bc(const BC & e)
{
  // ERROR:
  // if(isA(e))
  //   std::cerr << "A!" << std::endl;

  if(isB(e))
    std::cerr << "B!" << std::endl;
  if(isC(e))
    std::cerr << "C!" << std::endl;
}

int main() {
  AB a;
  a = A;
  AB b;
  b = B;
  ab(a);
  ab(b);
  ab(A);
  ab(B);
  // ab(C); // ERROR
  // bc(A); // ERROR
  bc(B);
  bc(C);
}

Vous pouvez probablement créer une macro pour générer le passe-partout. (Faites-moi savoir si vous le faites.)

Contrairement aux autres approches, celle-ci est en fait compatible avec le type et fonctionne avec l'ancien C++. Vous pouvez même créer des types sympas tels que boost::variant<int, A_t, B_t, boost::none>, par exemple, pour représenter une valeur pouvant être A, B, un entier ou rien qui correspond presque au niveau de sécurité de Haskell98.

Inconvénients à prendre en compte:

  • au moins avec l'ancien boost - je suis sur un système avec boost 1.33 - vous êtes limité à 20 articles dans votre variante; il y a un contournement cependant
  • affecte le temps de compilation
  • messages d'erreur insensés - mais c'est C++ pour vous

Mettre à jour

Ici, pour votre commodité est votre "bibliothèque" typesafe-enum. Collez cet en-tête:

#ifndef _TYPESAFE_ENUMS_H
#define _TYPESAFE_ENUMS_H
#include <string>
#include <boost/variant.hpp>

#define ITEM(NAME, VAL) \
struct NAME##_t { \
  std::string toStr() const { return std::string( #NAME ); } \
  int toInt() const { return VAL; } \
}; \
static const NAME##_t NAME = NAME##_t(); \
template <typename T> \
bool is##NAME(const T & x) { if(boost::get<NAME##_t>(&x)) return true; return false; } \


class toStr_visitor: public boost::static_visitor<std::string> {
public:
  template<typename T>
  std::string operator()(const T & a) const {
    return a.toStr();
  }
};

template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
inline static
std::string toStr(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
  return boost::apply_visitor(toStr_visitor(), a);
}

class toInt_visitor: public boost::static_visitor<int> {
public:
  template<typename T>
  int operator()(const T & a) const {
    return a.toInt();
  }
};

template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
inline static
int toInt(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
  return boost::apply_visitor(toInt_visitor(), a);
}

#define ENUM(...) \
typedef boost::variant<__VA_ARGS__>
#endif

Et utilisez-le comme:

ITEM(A, 0);
ITEM(B, 1);
ITEM(C, 2);

ENUM(A_t, B_t) AB;
ENUM(B_t, C_t) BC;

Notez que vous devez dire A_t au lieu de A dans la macro ENUM qui détruit une partie de la magie. Tant pis. En outre, notez qu’il existe désormais une fonction toStr et une fonction toInt pour répondre aux exigences des PO en matière de conversion simple en chaînes et en éléments. L'exigence que je n'arrive pas à comprendre est un moyen de parcourir les éléments. Faites-moi savoir si vous savez comment écrire une telle chose.

0
Michael Fox

Je suis en train d'écrire ma propre bibliothèque enum typesafe à https://bitbucket.org/chopsii/typesafe-enums

Je ne suis pas le développeur C++ le plus expérimenté de tous les temps, mais j'écris ceci en raison des faiblesses des répertoires d'emplacement de stockage BOOST. 

N'hésitez pas à vérifier et à les utiliser vous-même, mais ils ont quelques problèmes d'utilisabilité (espérons-le mineurs) et ne sont probablement pas du tout multi-plateformes.

S'il vous plaît contribuer si vous voulez. Ceci est ma première entreprise open source.

0
Lynden Shields