Forma genérica de convertir int a enum en C ++

81

¿Hay una manera genérica para echar inta enumen C++?

Si intcae en el rango de un enum, debería devolver un enumvalor; de lo contrario, arroje un exception. ¿Hay alguna forma de escribirlo genéricamente ? Se enum typedebería admitir más de uno .

Antecedentes: tengo un tipo de enumeración externa y no control sobre el código fuente. Me gustaría almacenar este valor en una base de datos y recuperarlo.

Leonid
fuente
enum e{x = 10000};¿En este caso 9999cae dentro del rango de enum?
Armen Tsirunyan
No, 9999no cae.
Leonid
9
Buena pregunta. En cuanto a cualquier "¿por qué?" que va a aparecer, permítanme decir "deserialización", parece una razón suficiente para mí. También me complacería escuchar una respuesta compilada en C ++ 0x para enum class.
Kos
9
"Rango" es la palabra incorrecta aquí, ¿tal vez "dominio"?
Constantin
boost :: numeric_cast <> lanza una excepción de desbordamiento positiva o negativa si el valor está fuera de los límites. Pero no estoy seguro de si también es válido para los tipos de enumeración. Puedes probar eso.
yasuser

Respuestas:

37

Lo obvio es anotar tu enumeración:

// generic code
#include <algorithm>

template <typename T>
struct enum_traits {};

template<typename T, size_t N>
T *endof(T (&ra)[N]) {
    return ra + N;
}

template<typename T, typename ValType>
T check(ValType v) {
    typedef enum_traits<T> traits;
    const T *first = traits::enumerators;
    const T *last = endof(traits::enumerators);
    if (traits::sorted) { // probably premature optimization
        if (std::binary_search(first, last, v)) return T(v);
    } else if (std::find(first, last, v) != last) {
        return T(v);
    }
    throw "exception";
}

// "enhanced" definition of enum
enum e {
    x = 1,
    y = 4,
    z = 10,
};

template<>
struct enum_traits<e> {
    static const e enumerators[];
    static const bool sorted = true;
};
// must appear in only one TU,
// so if the above is in a header then it will need the array size
const e enum_traits<e>::enumerators[] = {x, y, z};

// usage
int main() {
    e good = check<e>(1);
    e bad = check<e>(2);
}

Necesita que la matriz se mantenga actualizada e, lo cual es una molestia si no es el autor de e. Como dice Sjoerd, probablemente se pueda automatizar con cualquier sistema de compilación decente.

En cualquier caso, te enfrentas a 7.2 / 6:

Para una enumeración donde emin es el enumerador más pequeño y emax es el más grande, los valores de la enumeración son los valores del tipo subyacente en el rango de bmin a bmax, donde bmin y bmax son, respectivamente, los valores más pequeño y más grande del más pequeño. campo de bits que puede almacenar emin y emax. Es posible definir una enumeración que tenga valores no definidos por ninguno de sus enumeradores.

Por lo tanto, si no es el autor de e, puede que tenga o no una garantía de que los valores válidos de erealmente aparezcan en su definición.

Steve Jessop
fuente
22

Feo.

enum MyEnum { one = 1, two = 2 };

MyEnum to_enum(int n)
{
  switch( n )
  {
    case 1 :  return one;
    case 2 : return two;
  }
  throw something();
}

Ahora la verdadera pregunta. ¿Por qué necesitas esto? El código es feo, no es fácil de escribir (*?) Y no es fácil de mantener, y no es fácil de incorporar a su código. El código te dice que está mal. ¿Por qué luchar?

EDITAR:

Alternativamente, dado que las enumeraciones son tipos integrales en C ++:

enum my_enum_val = static_cast<MyEnum>(my_int_val);

pero esto es aún más feo que el anterior, mucho más propenso a errores y no lanzará como usted desea.

John Dibling
fuente
esto admite solo un tipo, MyEnum.
Simone
2
@Leonid: Que yo sepa, no se puede hacer de forma genérica. En algún nivel, cualquier solución que se le ocurra que hará throw(o hará algo especial) para tipos no válidos debe tener un interruptor como el que he publicado.
John Dibling
2
¿Por qué está esto -1'ed? Es la respuesta correcta. El hecho de que no sea la respuesta que algunos esperaban no significa que esté mal.
John Dibling
12
A static_cast<MyEnum>también funcionará, y debería preferirse sobre elreinterpret_cast<MyEnum>
Sjoerd
1
Este enfoque funciona bien, e incluso mejor si usa una herramienta para generar la función.
Nick
3

Si, como describe, los valores están en una base de datos, ¿por qué no escribir un generador de código que lea esta tabla y cree un archivo .hy .cpp con la enumeración y la to_enum(int)función?

Ventajas:

  • Fácil de agregar una to_string(my_enum)función.
  • Requiere poco mantenimiento
  • La base de datos y el código están sincronizados
Sjoerd
fuente
No hay control sobre el código fuente de tipo enumeración . Estoy de acuerdo en que se puede generar una instalación que implemente la conversión. Sin embargo, la instalación no estaría al tanto de ningún cambio / extensión realizado en el tipo de enumeración externa (a menos que se ejecute cada vez en el momento de la compilación).
Leonid
@Leonid Luego lee ese encabezado de enumeración y genera la to_enum(int)función basada en eso.
Sjoerd
@Leonid Cada sistema serio de gestión de proyectos, incluso make, puede comparar la fecha de dos archivos para ver si el generador debe volver a ejecutarse.
Sjoerd
Creo que iré con una solución más simple para el generador por ahora. Pero gracias por la idea.
Leonid
Usamos este esquema en nuestro lugar de trabajo, una herramienta genera el código .hpp a partir de una plantilla, la plantilla es mínima.
Nick
3

No, no hay introspección en C ++, ni hay ninguna función de "verificación de dominio" incorporada.

lucas
fuente
2

¿Qué piensas acerca de esto?

#include <iostream>
#include <stdexcept>
#include <set>
#include <string>

using namespace std;

template<typename T>
class Enum
{
public:
    static void insert(int value)
    {
        _set.insert(value);
    }

    static T buildFrom(int value)
    {
        if (_set.find(value) != _set.end()) {
            T retval;
            retval.assign(value);
            return retval;
        }
        throw std::runtime_error("unexpected value");
    }

    operator int() const { return _value; }

private:
    void assign(int value)
    {
        _value = value;
    }

    int _value;
    static std::set<int> _set;
};

template<typename T> std::set<int> Enum<T>::_set;

class Apples: public Enum<Apples> {};

class Oranges: public Enum<Oranges> {};

class Proxy
{
public:
    Proxy(int value): _value(value) {}

    template<typename T>
    operator T()
    {
        T theEnum;
        return theEnum.buildFrom(_value);
    }

    int _value;
};

Proxy convert(int value)
{
    return Proxy(value);
}

int main()
{    
    Apples::insert(4);
    Apples::insert(8);

    Apples a = convert(4); // works
    std::cout << a << std::endl; // prints 4

    try {
        Apples b = convert(9); // throws    
    }
    catch (std::exception const& e) {
        std::cout << e.what() << std::endl; // prints "unexpected value"
    }
    try {
        Oranges b = convert(4); // also throws  
    }
    catch (std::exception const& e) {
        std::cout << e.what() << std::endl; // prints "unexpected value"
    }
}

Luego puede usar el código que publiqué aquí para activar los valores.

Simone
fuente
Aún necesita agregar Apples::insert(4)algo, por lo que esto no tiene ninguna ventaja sobre un interruptor.
Sjoerd
1
Dijo que los valores provienen de una base de datos, por eso agregué un método de "inserción".
Simone
1

No debería querer que exista algo como lo que describe, me temo que hay problemas en el diseño de su código.

Además, asume que las enumeraciones vienen en un rango, pero ese no siempre es el caso:

enum Flags { one = 1, two = 2, four = 4, eigh = 8, big = 2000000000 };

Esto no está en un rango: incluso si fuera posible, ¿se supone que debe verificar cada número entero de 0 a 2 ^ n para ver si coinciden con el valor de alguna enumeración?

Simone
fuente
¿Cómo recuperarías los valores de enumeración de la base de datos? Los números enteros se conocen en tiempo de compilación, entonces, ¿por qué no es posible tener una conversión genérica basada en plantillas?
Leonid
2
@Leonid: Porque en algún nivel, necesitas tener un interruptor, como dije.
John Dibling
2
@Leonid Las plantillas no son una fórmula mágica para resolver todos los problemas que se te ocurran.
Sjoerd
John tiene razón. Necesita un tipo más complejo que una enumeración para hacer lo que quiere, creo que es factible con una jerarquía de clases.
Simone
Publiqué una solución que usa jerarquía de clases, compruébalo.
Simone
1

Si está preparado para enumerar sus valores de enumeración como parámetros de plantilla, puede hacerlo en C ++ 11 con plantillas varadic. Puede ver esto como algo bueno, ya que le permite aceptar subconjuntos de los valores de enumeración válidos en diferentes contextos; a menudo es útil al analizar códigos de fuentes externas.

Quizás no sea tan genérico como le gustaría, pero el código de verificación en sí es generalizado, solo necesita especificar el conjunto de valores. Este enfoque maneja huecos, valores arbitrarios, etc.

template<typename EnumType, EnumType... Values> class EnumCheck;

template<typename EnumType> class EnumCheck<EnumType>
{
public:
    template<typename IntType>
    static bool constexpr is_value(IntType) { return false; }
};

template<typename EnumType, EnumType V, EnumType... Next>
class EnumCheck<EnumType, V, Next...> : private EnumCheck<EnumType, Next...>
{
    using super = EnumCheck<EnumType, Next...>;

public:
    template<typename IntType>
    static bool constexpr is_value(IntType v)
    {
        return v == static_cast<typename std::underlying_type<EnumType>::type>(V) || super::is_value(v);
    }

    EnumType convert(IntType v)
    {
        if (!is_value(v)) throw std::runtime_error("Enum value out of range");
        return static_cast<EnumType>(v);
};

enum class Test {
    A = 1,
    C = 3,
    E = 5
};

using TestCheck = EnumCheck<Test, Test::A, Test::C, Test::E>;

void check_value(int v)
{
    if (TestCheck::is_value(v))
        printf("%d is OK\n", v);
    else
        printf("%d is not OK\n", v);
}

int main()
{
    for (int i = 0; i < 10; ++i)
        check_value(i);
}
Janm
fuente
Si bien este enlace puede responder la pregunta, es mejor incluir aquí las partes esenciales de la respuesta y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden dejar de ser válidas si cambia la página enlazada. - De la crítica
Tas
1
@Tas Es un enlace a una respuesta SO diferente; no tiene los mismos problemas que tiene un enlace externo. Actualizado de todos modos.
2016
0

C ++ 0x alternativa a la versión "fea", permite múltiples enumeraciones. Utiliza listas de inicializadores en lugar de conmutadores, un poco más limpio en mi opinión. Desafortunadamente, esto no soluciona la necesidad de codificar los valores de enumeración.

#include <cassert>  // assert

namespace  // unnamed namespace
{
    enum class e1 { value_1 = 1, value_2 = 2 };
    enum class e2 { value_3 = 3, value_4 = 4 };

    template <typename T>
    int valid_enum( const int val, const T& vec )
    {
        for ( const auto item : vec )
            if ( static_cast<int>( item ) == val ) return val;

        throw std::exception( "invalid enum value!" );  // throw something useful here
    }   // valid_enum
}   // ns

int main()
{
    // generate list of valid values
    const auto e1_valid_values = { e1::value_1, e1::value_2 };
    const auto e2_valid_values = { e2::value_3, e2::value_4 };

    auto result1 = static_cast<e1>( valid_enum( 1, e1_valid_values ) );
    assert( result1 == e1::value_1 );

    auto result2 = static_cast<e2>( valid_enum( 3, e2_valid_values ) );
    assert( result2 == e2::value_3 );

    // test throw on invalid value
    try
    {
        auto result3 = static_cast<e1>( valid_enum( 9999999, e1_valid_values ) );
        assert( false );
    }
    catch ( ... )
    {
        assert( true );
    }
}
Tom
fuente