Comment convertir automatiquement enum fortement typé en int?

165
#include <iostream>

struct a {
  enum LOCAL_A { A1, A2 };
};
enum class b { B1, B2 };

int foo(int input) { return input; }

int main(void) {
  std::cout << foo(a::A1) << std::endl;
  std::cout << foo(static_cast<int>(b::B2)) << std::endl;
}

C'est a::LOCAL_Ace que l'énumération fortement typée essaie de réaliser, mais il y a une petite différence: les énumérations normales peuvent être converties en type entier, tandis que les énumérations fortement typées ne peuvent pas le faire sans un cast.

Alors, existe-t-il un moyen de convertir une valeur enum fortement typée en un type entier sans conversion? Si oui, comment?

BЈовић
la source

Réponses:

134

Enums fortement typés visant à résoudre plusieurs problèmes et pas seulement le problème de portée comme vous l'avez mentionné dans votre question:

  1. Fournit une sécurité de type, éliminant ainsi la conversion implicite en entier par promotion intégrale.
  2. Spécifiez les types sous-jacents.
  3. Fournir une portée solide.

Ainsi, il est impossible de convertir implicitement une énumération fortement typée en entiers, ou même son type sous-jacent - c'est l'idée. Vous devez donc utiliser static_castpour rendre la conversion explicite.

Si votre seul problème est la portée et que vous voulez vraiment avoir une promotion implicite vers des entiers, il vaut mieux utiliser une énumération non fortement typée avec la portée de la structure dans laquelle elle est déclarée.

LF
la source
2
C'est un autre exemple étrange de «nous savons mieux ce que vous voulez faire» des créateurs C ++. Les énumérations conventionnelles (à l'ancienne) avaient des tonnes d'avantages comme la conversion implicite en index, l'utilisation transparente d'opérations au niveau du bit, etc. Les nouvelles énumérations de style ont ajouté une très bonne chose de portée, mais ... Vous ne pouvez pas utiliser simplement cette chose (même avec explicite spécification de type sous-jacent!). Alors maintenant, vous êtes soit obligé d'utiliser des énumérations de style ancien avec des astuces comme les mettre dans struct, soit de créer des solutions de contournement les plus laides pour les nouvelles énumérations, comme créer votre propre wrapper autour de std :: vector juste pour surmonter cette chose CAST. aucun commentaire
avtomaton
152

Comme d'autres l'ont dit, vous ne pouvez pas avoir de conversion implicite, et c'est par conception.

Si vous le souhaitez, vous pouvez éviter d'avoir à spécifier le type sous-jacent dans la distribution.

template <typename E>
constexpr typename std::underlying_type<E>::type to_underlying(E e) noexcept {
    return static_cast<typename std::underlying_type<E>::type>(e);
}

std::cout << foo(to_underlying(b::B2)) << std::endl;
R. Martinho Fernandes
la source
75

Une version C ++ 14 de la réponse fournie par R. Martinho Fernandes serait:

#include <type_traits>

template <typename E>
constexpr auto to_underlying(E e) noexcept
{
    return static_cast<std::underlying_type_t<E>>(e);
}

Comme pour la réponse précédente, cela fonctionnera avec n'importe quel type d'énumération et de type sous-jacent. J'ai ajouté le noexceptmot - clé car il ne lèvera jamais d'exception.


Mise à jour
Cela apparaît également dans Effective Modern C ++ par Scott Meyers . Voir l'article 10 (il est détaillé dans les dernières pages de l'article dans ma copie du livre).

Squelette de classe
la source
18
#include <cstdlib>
#include <cstdio>
#include <cstdint>

#include <type_traits>

namespace utils
{

namespace details
{

template< typename E >
using enable_enum_t = typename std::enable_if< std::is_enum<E>::value, 
                                               typename std::underlying_type<E>::type 
                                             >::type;

}   // namespace details


template< typename E >
constexpr inline details::enable_enum_t<E> underlying_value( E e )noexcept
{
    return static_cast< typename std::underlying_type<E>::type >( e );
}   


template< typename E , typename T>
constexpr inline typename std::enable_if< std::is_enum<E>::value &&
                                          std::is_integral<T>::value, E
                                         >::type 
 to_enum( T value ) noexcept 
 {
     return static_cast<E>( value );
 }

} // namespace utils




int main()
{
    enum class E{ a = 1, b = 3, c = 5 };

    constexpr auto a = utils::underlying_value(E::a);
    constexpr E    b = utils::to_enum<E>(5);
    constexpr auto bv = utils::underlying_value(b);

    printf("a = %d, b = %d", a,bv);
    return 0;
}
Khurshid Normuradov
la source
3
Cela ne réduit pas la saisie et ne rend pas le code plus propre et a les effets secondaires de rendre plus difficile la recherche de telles conversions implicites dans les grands projets. Static_cast serait plus facile à rechercher à l'échelle du projet que ces constructions.
Atul Kumar
3
@AtulKumar En quoi la recherche de static_cast est-elle plus facile que la recherche de to_enum?
Johann Gerell
1
Cette réponse a besoin d'explications et de documentation.
Courses de légèreté en orbite
17

Non, il n'y a pas de moyen naturel .

En fait, l'une des motivations derrière avoir fortement tapé enum classen C ++ 11 est d'empêcher leur conversion silencieuse vers int.

iammilind
la source
Jetez un œil à la réponse de Khurshid Normuradov. Il vient de la «manière naturelle» et est tout à fait comme prévu dans «Le langage de programmation C ++ (4e éd.)». Cela ne vient pas de manière «automatique», et c'est bien.
PapaAtHome
@PapaAtHome Je ne comprends pas l'intérêt de cela sur static_cast. Pas beaucoup de changement dans la frappe ou la propreté du code. Quelle est la manière naturelle ici? Une fonction qui renvoie une valeur?
Atul Kumar
1
@ user2876962 L'avantage, pour moi, c'est que ce n'est pas automatique ou «silencieux» comme le dit Iammilind. Cela évite de trouver des erreurs. Vous pouvez toujours faire un casting mais vous êtes obligé d'y penser. De cette façon, vous savez ce que vous faites. Pour moi, cela fait partie d'une habitude de «codage sûr». Je préfère qu'aucune conversion ne soit effectuée automatiquement, car il y a un risque que cela introduise une erreur. Quelques changements dans C ++ 11 liés au système de types tombent dans cette catégorie si vous me le demandez.
PapaAtHome
17

La raison de l'absence de conversion implicite (par conception) a été donnée dans d'autres réponses.

J'utilise personnellement unaire operator+pour la conversion des classes enum vers leur type sous-jacent:

template <typename T>
constexpr auto operator+(T e) noexcept
    -> std::enable_if_t<std::is_enum<T>::value, std::underlying_type_t<T>>
{
    return static_cast<std::underlying_type_t<T>>(e);
}

Ce qui donne assez peu de "surcharge de frappe":

std::cout << foo(+b::B2) << std::endl;

Où j'utilise en fait une macro pour créer des énumérations et des fonctions d'opérateur en un seul coup.

#define UNSIGNED_ENUM_CLASS(name, ...) enum class name : unsigned { __VA_ARGS__ };\
inline constexpr unsigned operator+ (name const val) { return static_cast<unsigned>(val); }
Pixelchimiste
la source
13

J'espère que cela vous aide ou aide quelqu'un d'autre

enum class EnumClass : int //set size for enum
{
    Zero, One, Two, Three, Four
};

union Union //This will allow us to convert
{
    EnumClass ec;
    int i;
};

int main()
{
using namespace std;

//convert from strongly typed enum to int

Union un2;
un2.ec = EnumClass::Three;

cout << "un2.i = " << un2.i << endl;

//convert from int to strongly typed enum
Union un;
un.i = 0; 

if(un.ec == EnumClass::Zero) cout << "True" << endl;

return 0;
}
vis.15
la source
33
C'est ce qu'on appelle "poinçonnage de type" et bien que supporté par certains compilateurs n'est pas portable, comme le standard C ++ dit qu'après avoir défini, un.ic'est le "membre actif" et vous ne pouvez lire que le membre actif d'une union.
Jonathan Wakely
6
@JonathanWakely Vous avez techniquement raison, mais je n'ai jamais vu un compilateur où cela ne fonctionne pas de manière fiable. Des trucs comme celui-ci, les syndicats anonymes et #pragma une fois sont des normes de facto.
BigSandwich
5
Pourquoi utiliser quelque chose que la norme interdit explicitement, alors qu'un simple casting fera l'affaire? C'est tout simplement faux.
Paul Groke
1
Techniquement correct ou pas, pour moi c'est beaucoup plus lisible que d'autres solutions trouvées ici. Et ce qui est plus important pour moi, il peut être utilisé pour résoudre non seulement la sérialisation, mais aussi la désérialisation de la classe enum avec facilité et un format lisible.
Marcin Waśniowski
6
Je désespère absolument qu'il y ait des gens qui considèrent ce comportement désordonné indéfini "bien plus lisible" qu'un simple static_cast.
underscore_d
13

La réponse courte est que vous ne pouvez pas, comme le soulignent les articles ci-dessus. Mais pour mon cas, je ne voulais tout simplement pas encombrer l'espace de noms mais avoir toujours des conversions implicites, alors j'ai juste fait:

#include <iostream>

using namespace std;

namespace Foo {
   enum Foo { bar, baz };
}

int main() {
   cout << Foo::bar << endl; // 0
   cout << Foo::baz << endl; // 1
   return 0;
}

Le type d'espacement de noms ajoute une couche de sécurité de type alors que je n'ai pas à convertir statiquement les valeurs d'énumération au type sous-jacent.

solstice333
la source
3
Il n'ajoute aucune sécurité de type (en effet, vous venez de supprimer la sécurité de type) - il ajoute seulement la portée du nom.
Courses de légèreté en orbite
@LightnessRacesinOrbit oui je suis d'accord. J'ai menti. Techniquement, pour être exact, le type se trouve juste sous un espace de nom / une portée et se qualifie pleinement pour Foo::Foo. Les membres peuvent être accédés comme Foo::baret Foo::bazet peuvent être implicitement castés (et donc pas beaucoup de sécurité de type). Il est probablement préférable d'utiliser presque toujours des classes enum, surtout si vous démarrez un nouveau projet.
solstice333
6

Cela semble impossible avec le natif enum class, mais vous pouvez probablement vous moquer d'un enum classavec un class:

Dans ce cas,

enum class b
{
    B1,
    B2
};

serait équivalent à:

class b {
 private:
  int underlying;
 public:
  static constexpr int B1 = 0;
  static constexpr int B2 = 1;
  b(int v) : underlying(v) {}
  operator int() {
      return underlying;
  }
};

Ceci est en grande partie équivalent à l'original enum class. Vous pouvez retourner directement b::B1pour dans une fonction avec le type de retour b. Vous pouvez faire switch caseavec, etc.

Et dans l'esprit de cet exemple, vous pouvez utiliser des modèles (éventuellement avec d'autres choses) pour généraliser et se moquer de tout objet possible défini par la enum classsyntaxe.

Colliot
la source
mais B1 et B2 doivent être définis en dehors de la classe ... ou cela est inutilisable pour case - header.h <- class b - main.cpp <---- myvector.push_back (B1)
Fl0
Cela ne devrait-il pas être "static constexpr b" au lieu de "static constexpr int '? Sinon, b :: B1 est juste un int sans sécurité de type du tout.
Some Guy
4

Comme beaucoup l'ont dit, il n'y a aucun moyen de convertir automatiquement sans ajouter des frais généraux et trop de complexité, mais vous pouvez réduire un peu votre frappe et la rendre meilleure en utilisant des lambdas si certains cast seront un peu utilisés dans un scénario. Cela ajouterait un peu de surcharge de fonction, mais rendra le code plus lisible par rapport aux longues chaînes static_cast comme on peut le voir ci-dessous. Cela peut ne pas être utile à l'échelle du projet, mais uniquement à l'échelle de la classe.

#include <bitset>
#include <vector>

enum class Flags { ......, Total };
std::bitset<static_cast<unsigned int>(Total)> MaskVar;
std::vector<Flags> NewFlags;

-----------
auto scui = [](Flags a){return static_cast<unsigned int>(a); };

for (auto const& it : NewFlags)
{
    switch (it)
    {
    case Flags::Horizontal:
        MaskVar.set(scui(Flags::Horizontal));
        MaskVar.reset(scui(Flags::Vertical)); break;
    case Flags::Vertical:
        MaskVar.set(scui(Flags::Vertical));
        MaskVar.reset(scui(Flags::Horizontal)); break;

   case Flags::LongText:
        MaskVar.set(scui(Flags::LongText));
        MaskVar.reset(scui(Flags::ShorTText)); break;
    case Flags::ShorTText:
        MaskVar.set(scui(Flags::ShorTText));
        MaskVar.reset(scui(Flags::LongText)); break;

    case Flags::ShowHeading:
        MaskVar.set(scui(Flags::ShowHeading));
        MaskVar.reset(scui(Flags::NoShowHeading)); break;
    case Flags::NoShowHeading:
        MaskVar.set(scui(Flags::NoShowHeading));
        MaskVar.reset(scui(Flags::ShowHeading)); break;

    default:
        break;
    }
}
Atul Kumar
la source
2

Le comité C ++ a fait un pas en avant (délimitation des énumérations hors de l'espace de noms global) et cinquante pas en arrière (pas de désintégration de type enum en entier). Malheureusement, enum classn'est tout simplement pas utilisable si vous avez besoin de la valeur de l'énumération de manière non symbolique.

La meilleure solution est de ne pas l'utiliser du tout, mais de définir vous-même l'énumération en utilisant un espace de noms ou une structure. A cet effet, ils sont interchangeables. Vous devrez taper un peu plus lorsque vous vous référez au type enum lui-même, mais ce ne sera probablement pas souvent.

struct TextureUploadFormat {
    enum Type : uint32 {
        r,
        rg,
        rgb,
        rgba,
        __count
    };
};

// must use ::Type, which is the extra typing with this method; beats all the static_cast<>()
uint32 getFormatStride(TextureUploadFormat::Type format){
    const uint32 formatStride[TextureUploadFormat::__count] = {
        1,
        2,
        3,
        4
    };
    return formatStride[format]; // decays without complaint
}
Anne Quinn
la source