¿Cómo convertir automáticamente una enumeración fuertemente tipada 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;
}

Esto a::LOCAL_Aes lo que la enumeración fuertemente tipada está tratando de lograr, pero hay una pequeña diferencia: las enumeraciones normales se pueden convertir en un tipo entero, mientras que las enumeraciones fuertemente tipadas no pueden hacerlo sin una conversión.

Entonces, ¿hay alguna manera de convertir un valor de enumeración fuertemente tipado en un tipo entero sin conversión? Si es así, ¿cómo?

BЈовић
fuente

Respuestas:

134

Enumeraciones fuertemente tipadas con el objetivo de resolver múltiples problemas y no solo el problema de alcance como mencionó en su pregunta:

  1. Proporcione seguridad de tipo, eliminando así la conversión implícita a entero mediante la promoción integral.
  2. Especificar tipos subyacentes.
  3. Proporcione un alcance sólido.

Por lo tanto, es imposible convertir implícitamente una enumeración fuertemente tipada a enteros, o incluso su tipo subyacente, esa es la idea. Por lo tanto, debe usar static_castpara hacer que la conversión sea explícita.

Si su único problema es el alcance y realmente desea tener una promoción implícita a los enteros, es mejor que use una enumeración no fuertemente tipada con el alcance de la estructura en la que se declara.

LF
fuente
2
Ese es otro extraño ejemplo de "sabemos mejor lo que quieres hacer" de los creadores de C ++. Las enumeraciones convencionales (estilo antiguo) tuvieron muchos beneficios, como la conversión implícita a índices, el uso continuo de operaciones bit a bit, etc. Las enumeraciones de estilo nuevo agregaron algo realmente excelente, pero ... No se puede utilizar solo esa cosa (incluso con explícito especificación de tipo subyacente!). Entonces, ahora estás obligado a usar enumeraciones de estilo antiguo con trucos como ponerlas en una estructura o crear soluciones más feas para nuevas enumeraciones como crear tu propio contenedor alrededor de std :: vector solo para superar esa cosa CAST. sin comentarios
avtomaton
152

Como han dicho otros, no puede tener una conversión implícita, y eso es por diseño.

Si lo desea, puede evitar la necesidad de especificar el tipo subyacente en el reparto.

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
fuente
75

Una versión C ++ 14 de la respuesta proporcionada por R. Martinho Fernandes sería:

#include <type_traits>

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

Al igual que con la respuesta anterior, esto funcionará con cualquier tipo de enumeración y tipo subyacente. He agregado la noexceptpalabra clave ya que nunca arrojará una excepción.


Actualización
Esto también aparece en Effective Modern C ++ de Scott Meyers . Vea el ítem 10 (se detalla en las páginas finales del ítem dentro de mi copia del libro).

Esqueleto de clase
fuente
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
fuente
3
Esto no reduce la escritura ni hace que el código sea más limpio y tiene los efectos secundarios de dificultar la búsqueda de conversiones implícitas en grandes proyectos. Static_cast sería más fácil de buscar en todo el proyecto que estas construcciones.
Atul Kumar
3
@AtulKumar ¿Cómo es más fácil buscar static_cast que buscar to_enum?
Johann Gerell
1
Esta respuesta necesita alguna explicación y documentación.
ligereza corre en órbita el
17

No. No hay forma natural .

De hecho, una de las motivaciones detrás de haber escrito fuertemente enum classen C ++ 11 es evitar su conversión silenciosa a int.

iammilind
fuente
Echa un vistazo a la respuesta de Khurshid Normuradov. Viene de la 'forma natural' y es muy parecido a lo que se pretende en 'El lenguaje de programación C ++ (4a ed.)' No viene de forma automática, y eso es bueno.
PapaAtHome
@PapaAtHome No entiendo el beneficio de eso sobre static_cast. No hay muchos cambios en la escritura o la limpieza del código. ¿Cuál es la forma natural aquí? ¿Una función que devuelve valor?
Atul Kumar
1
@ user2876962 El beneficio, para mí, es que no es automático o 'silencioso' como lo expresa Iammilind. Eso evita dificultades para encontrar errores. Todavía puedes hacer un reparto, pero te ves obligado a pensarlo. De esa manera sabes lo que estás haciendo. Para mí es parte de un hábito de "codificación segura". Prefiero que no se realicen conversiones automáticas si hay una pequeña posibilidad de que pueda introducir un error. Un buen número de cambios en C ++ 11 relacionados con el sistema de tipos entran en esta categoría si me preguntas.
PapaAtHome
17

La razón de la ausencia de conversión implícita (por diseño) se dio en otras respuestas.

Yo personalmente uso unary operator+para la conversión de clases enum a su tipo subyacente:

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);
}

Lo que da bastante poca "sobrecarga de escritura":

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

Donde realmente uso una macro para crear enumeraciones y el operador funciona de una sola vez.

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

Espero que esto te ayude a ti oa alguien más

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
fuente
33
Esto se llama "punteo de tipo" y, aunque algunos compiladores lo admiten, no es portátil, ya que el estándar de C ++ dice que después de configurar un.iese es el "miembro activo" y solo puede leer el miembro activo de una unión.
Jonathan Wakely
66
@ JonathanWakely Eres técnicamente correcto, pero nunca he visto un compilador en el que esto no funciona de manera confiable. Cosas como esta, los sindicatos anónimos y #pragma alguna vez son estándares de facto.
BigSandwich
55
¿Por qué usar algo que el estándar prohíbe explícitamente, cuando lo hará un simple lanzamiento? Esto es simplemente incorrecto.
Paul Groke
1
Técnicamente correcto o no, para mí es mucho más legible que otras soluciones encontradas aquí. Y lo que es más importante para mí, puede usarse para resolver no solo la serialización, sino también la deserialización de la clase enum con facilidad y un formato legible.
Marcin Waśniowski el
66
Estoy absolutamente desesperado de que haya personas que consideren este comportamiento indefinido desordenado "mucho más legible" que simple static_cast.
underscore_d
13

La respuesta corta es que no puedes, como señalan las publicaciones anteriores. Pero para mi caso, simplemente no quería saturar el espacio de nombres, pero aún tenía conversiones implícitas, así que simplemente lo hice:

#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;
}

El tipo de espacio de nombres agrega una capa de seguridad de tipo mientras que no tengo que convertir estáticamente ningún valor de enumeración al tipo subyacente.

solsticio333
fuente
3
No agrega ningún tipo de seguridad (de hecho, acaba de eliminar el tipo de seguridad), solo agrega el alcance del nombre.
ligereza corre en órbita el
@LightnessRacesinOrbit sí, estoy de acuerdo. Mentí. Técnicamente, para ser exactos, el tipo está justo debajo de un espacio de nombre / alcance y califica completamente para Foo::Foo. Se puede acceder a los miembros como Foo::bary Foo::bazse puede emitir implícitamente (y por lo tanto, no hay mucha seguridad de tipo). Probablemente sea mejor usar casi siempre clases de enumeración, especialmente si comienza un nuevo proyecto.
solsticio333
6

Esto parece imposible con el nativo enum class, pero probablemente puedas burlarte de un enum classcon un class:

En este caso,

enum class b
{
    B1,
    B2
};

sería equivalente a:

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;
  }
};

Esto es principalmente equivalente al original enum class. Puede regresar directamente b::B1en una función con tipo de retorno b. Puedes hacer switch casecon eso, etc.

Y en el espíritu de este ejemplo, puede usar plantillas (posiblemente junto con otras cosas) para generalizar y burlarse de cualquier objeto posible definido por la enum classsintaxis.

Colliot
fuente
pero B1 y B2 deben definirse fuera de la clase ... o esto no se puede utilizar para case - header.h <- class b - main.cpp <---- myvector.push_back (B1)
Fl0
¿No debería ser "static constexpr b" en lugar de "static constexpr int '? De lo contrario, b :: B1 es solo un int sin ningún tipo de seguridad.
Algún tipo
4

Como muchos dijeron, no hay forma de convertir automáticamente sin agregar gastos generales y demasiada complejidad, pero puede reducir un poco su escritura y hacer que se vea mejor usando lambdas si algún elenco se usará demasiado en un escenario. Eso agregaría un poco de función de llamada general, pero hará que el código sea más legible en comparación con las largas cadenas static_cast como se puede ver a continuación. Esto puede no ser útil para todo el proyecto, sino solo para toda la clase.

#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
fuente
2

El comité de C ++ dio un paso adelante (delimitando las enumeraciones del espacio de nombres global) y cincuenta pasos hacia atrás (sin disminución del tipo de enumeración a entero). Lamentablemente, enum classsimplemente no es utilizable si necesita el valor de la enumeración de alguna manera no simbólica.

La mejor solución es no usarlo en absoluto y, en su lugar, examinar la enumeración usted mismo utilizando un espacio de nombres o una estructura. Para este propósito, son intercambiables. Tendrá que escribir un poco más cuando se refiera al tipo de enumeración en sí, pero eso probablemente no sea frecuente.

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
fuente