Ogólny sposób rzutowania int na wyliczenie w C ++

82

Czy istnieje sposób ogólny do oddania intdo enumw C++?

Jeśli intmieści się w zakresie an enum, powinien zwrócić enumwartość, w przeciwnym razie należy zgłosić exception. Czy jest sposób, aby napisać to ogólnie ? enum typeNależy wspierać więcej niż jeden .

Tło: mam zewnętrzny typ wyliczenia i nie mam kontroli nad kodem źródłowym. Chciałbym przechowywać tę wartość w bazie danych i ją odzyskać.

Leonid
źródło
enum e{x = 10000};czy w tym przypadku 9999mieści się w zakresie enum?
Armen Tsirunyan
Nie, 9999nie spada.
Leonid
9
Dobre pytanie. A jeśli chodzi o „dlaczego?” co się pojawi, powiem tylko „deserializacja” - wydaje mi się wystarczającym powodem. Z przyjemnością usłyszę również odpowiedź zgodną z C ++ 0x enum class.
Kos
9
„Zakres” to niewłaściwe słowo, może „domena”?
Constantin
boost :: numeric_cast <> zgłasza dodatni lub ujemny wyjątek przepełnienia, jeśli wartość przekracza granice. Ale nie jestem pewien, czy sprawdza się również w przypadku typów wyliczeniowych. Możesz tego spróbować.
yasouser

Odpowiedzi:

38

Oczywistą rzeczą jest dodanie adnotacji do wyliczenia:

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

Musisz aktualizować tablicę e, co jest uciążliwe, jeśli nie jesteś jej autorem e. Jak mówi Sjoerd, prawdopodobnie można to zautomatyzować za pomocą dowolnego porządnego systemu kompilacji.

W każdym razie masz do czynienia z 7,2 / 6:

W przypadku wyliczenia, w którym emin jest najmniejszym modułem wyliczającym, a emax jest największym, wartościami wyliczenia są wartości typu bazowego w zakresie od bmin do bmax, gdzie bmin i bmax to odpowiednio najmniejsze i największe wartości najmniejszej pole bitowe, które może przechowywać emin i emax. Możliwe jest zdefiniowanie wyliczenia, które ma wartości nie zdefiniowane przez żaden z jego modułów wyliczających.

Więc jeśli nie jesteś autorem e, możesz lub nie mieć gwarancji, że prawidłowe wartości erzeczywiście pojawiają się w jego definicji.

Steve Jessop
źródło
22

Brzydki.

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

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

A teraz prawdziwe pytanie. Dlaczego tego potrzebujesz? Kod jest brzydki, niełatwy do napisania (*?), Niełatwy w utrzymaniu i niełatwy do włączenia do kodu. Kod, który mówi ci, że jest zły. Po co z tym walczyć?

EDYTOWAĆ:

Alternatywnie, biorąc pod uwagę, że wyliczenia są typami całkowitymi w C ++:

enum my_enum_val = static_cast<MyEnum>(my_int_val);

ale to jest jeszcze brzydsze niż powyżej, znacznie bardziej podatne na błędy i nie wyrzuci tak, jak chcesz.

John Dibling
źródło
obsługuje tylko jeden typ, MyEnum.
Simone,
2
@Leonid: O ile wiem, nie można tego zrobić ogólnie. Na pewnym poziomie każde rozwiązanie, które wymyślisz, będzie throw(lub zrobi coś specjalnego) dla nieprawidłowych typów, musi mieć przełącznik, tak jak opublikowałem.
John Dibling
2
Dlaczego to jest -ed? To poprawna odpowiedź. To, że nie jest to odpowiedź, na którą niektórzy liczyli, nie oznacza, że ​​jest źle.
John Dibling,
12
A static_cast<MyEnum>również się sprawdzi i powinno być preferowane nadreinterpret_cast<MyEnum>
Sjoerd,
1
To podejście działa dobrze, a nawet lepiej, jeśli używasz narzędzia do generowania funkcji.
Nick
3

Jeśli, jak opisujesz, wartości znajdują się w bazie danych, dlaczego nie napisać generatora kodu, który odczytuje tę tabelę i tworzy plik .h i .cpp z wyliczeniem i to_enum(int)funkcją?

Zalety:

  • Łatwo dodać to_string(my_enum)funkcję.
  • Wymagana niewielka konserwacja
  • Baza danych i kod są zsynchronizowane
Sjoerd
źródło
Nie ma kontroli nad kodem źródłowym typu wyliczenia . Zgadzam się, że można wygenerować obiekt, który implementuje konwersję. Jednak obiekt nie byłby świadomy jakichkolwiek zmian / rozszerzeń wprowadzonych do zewnętrznego typu wyliczenia (chyba że jest wykonywany za każdym razem w czasie kompilacji).
Leonid
@Leonid Następnie przeczytaj nagłówek wyliczenia i wygeneruj na jego to_enum(int)podstawie funkcję.
Sjoerd,
@Leonid Nawet każdy poważny system zarządzania projektami makemoże porównać datę dwóch plików, aby sprawdzić, czy generator musi zostać ponownie uruchomiony.
Sjoerd,
Myślę, że na razie pójdę z prostszym rozwiązaniem do generatora. Ale dzięki za pomysł.
Leonid
Używamy tego schematu w naszym miejscu pracy, narzędzie generuje kod .hpp z szablonu, szablon jest minimalny.
Nick
3

Nie - w C ++ nie ma introspekcji ani wbudowanej funkcji „sprawdzania domeny”.

Łukasz
źródło
2

Co o tym myślisz?

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

Następnie możesz użyć kodu, który tutaj opublikowałem , aby włączyć wartości.

Simone
źródło
Nadal musisz Apples::insert(4)gdzieś dodać , więc nie ma to przewagi nad przełącznikiem.
Sjoerd
1
Powiedział, że wartości pochodzą z bazy danych, dlatego dodałem metodę „wstaw”.
Simone
1

Nie powinieneś chcieć, aby coś takiego jak to, co opisujesz, istniało, obawiam się, że są problemy w projekcie twojego kodu.

Zakładasz również, że wyliczenia mieszczą się w zakresie, ale nie zawsze tak jest:

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

To nie jest w zakresie: nawet gdyby było to możliwe, czy powinieneś sprawdzić każdą liczbę całkowitą od 0 do 2 ^ n, aby zobaczyć, czy pasuje do jakiejś wartości wyliczenia?

Simone
źródło
w jaki sposób w przeciwnym razie pobrałbyś wartości wyliczenia z bazy danych? Liczby całkowite są znane w czasie kompilacji, więc dlaczego nie można mieć ogólnej konwersji opartej na szablonach?
Leonid
2
@Leonid: Ponieważ na pewnym poziomie musisz mieć przełącznik, jak powiedziałem.
John Dibling
2
Szablony @Leonid nie są srebrnym środkiem do rozwiązania każdego problemu, jaki przyjdzie Ci do głowy.
Sjoerd
John ma rację. Aby robić to, co chcesz, potrzebujesz bardziej złożonego typu niż wyliczenie. Myślę, że jest to wykonalne w przypadku hierarchii klas.
Simone
Opublikowałem rozwiązanie, które wykorzystuje hierarchię klas, sprawdź to.
Simone
1

Jeśli jesteś przygotowany do umieszczenia wartości wyliczenia jako parametrów szablonu, możesz to zrobić w C ++ 11 za pomocą szablonów waradycznych. Możesz potraktować to jako dobrą rzecz, umożliwiając akceptowanie podzbiorów prawidłowych wartości wyliczenia w różnych kontekstach; często przydatne podczas analizowania kodów ze źródeł zewnętrznych.

Być może nie jest tak ogólny, jak byś chciał, ale sam kod sprawdzający jest uogólniony, wystarczy określić zestaw wartości. To podejście obsługuje luki, arbitralne wartości itp.

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
źródło
Chociaż ten link może odpowiedzieć na pytanie, lepiej jest zawrzeć tutaj zasadnicze części odpowiedzi i podać link do odniesienia. Odpowiedzi zawierające tylko łącze mogą stać się nieprawidłowe, jeśli połączona strona ulegnie zmianie. - Z recenzji
Tas
1
@Tas Jest to link do innej odpowiedzi SO - nie ma tych samych problemów, co łącze zewnętrzne. W każdym razie zaktualizowany.
janm
0

C ++ 0x alternatywa dla "brzydkiej" wersji, pozwala na wiele wyliczeń. Używa list inicjalizujących zamiast przełączników, nieco czystszy IMO. Niestety, nie pozwala to obejść potrzeby sztywnego kodowania wartości wyliczeniowych.

#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 );
    }
}
Tomek
źródło