Jak automatycznie przekonwertować silnie wpisane wyliczenie na 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;
}

To a::LOCAL_Ajest to, co próbuje osiągnąć silnie typowane wyliczenie, ale jest mała różnica: normalne wyliczenia można przekonwertować na typ całkowity, podczas gdy silnie typowane wyliczenia nie mogą tego zrobić bez rzutowania.

Czy jest więc sposób na przekonwertowanie wartości wyliczenia o jednoznacznie określonym typie na typ całkowity bez rzutowania? Jeśli tak, w jaki sposób?

BЈовић
źródło

Odpowiedzi:

134

Silnie wpisane wyliczenia mające na celu rozwiązanie wielu problemów, a nie tylko problem dotyczący zakresu, jak wspomniałeś w swoim pytaniu:

  1. Zapewnij bezpieczeństwo typu, eliminując w ten sposób niejawną konwersję na liczbę całkowitą przez integralną promocję.
  2. Określ typy podstawowe.
  3. Zapewnij silny zakres.

W związku z tym niemożliwe jest niejawne przekonwertowanie wyliczenia o silnym typie na liczby całkowite, a nawet na jego typ bazowy - o to chodzi. Musisz więc użyć, static_castaby wyraźnie zaznaczyć konwersję.

Jeśli jedynym problemem jest określanie zakresu i naprawdę chcesz mieć niejawną promocję na liczby całkowite, lepiej jest użyć niezbyt silnie wpisanego wyliczenia z zakresem struktury, w której jest zadeklarowany.

LF
źródło
2
To kolejny dziwny przykład „wiemy lepiej, co chcesz zrobić” od twórców C ++. Konwencjonalne (w starym stylu) wyliczenia miały mnóstwo korzyści, takich jak niejawna konwersja do indeksów, bezproblemowe użycie operacji bitowych itp. Nowe wyliczenia w nowym stylu dodały naprawdę świetną funkcję określania zakresu, ale ... Nie możesz używać tylko tego (nawet z jawnym podstawowa specyfikacja typu!). Więc teraz albo jesteś zmuszony używać wyliczeń w starym stylu ze sztuczkami, takimi jak umieszczanie ich w struct, lub tworzyć najbrzydsze obejścia dla nowych wyliczeń, takie jak tworzenie własnego opakowania wokół std :: vector, aby przezwyciężyć to CAST. brak komentarzy
avtomaton
152

Jak powiedzieli inni, nie można mieć niejawnej konwersji, a to jest zamierzone.

Jeśli chcesz, możesz uniknąć konieczności określania typu podstawowego w rzutowaniu.

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
źródło
75

Wersja C ++ 14 odpowiedzi udzielonej przez R.Martinho Fernandes byłaby:

#include <type_traits>

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

Podobnie jak w przypadku poprzedniej odpowiedzi, zadziała to z każdym rodzajem wyliczenia i typem bazowym. Dodałem noexceptsłowo kluczowe, ponieważ nigdy nie zgłosi wyjątku.


Aktualizacja
Pojawia się również w Effective Modern C ++ autorstwa Scotta Meyersa . Zobacz punkt 10 (jest szczegółowo opisany na ostatnich stronach pozycji w moim egzemplarzu książki).

Szkielet klasy
źródło
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
źródło
3
Nie ogranicza to pisania ani nie czyni kodu czystszym i ma skutki uboczne utrudniające znalezienie takich niejawnych konwersji w dużych projektach. Static_cast byłoby łatwiejsze do przeszukiwania całego projektu niż te konstrukcje.
Atul Kumar
3
@AtulKumar W jaki sposób wyszukiwanie static_cast jest łatwiejsze niż wyszukiwanie to_enum?
Johann Gerell,
1
Ta odpowiedź wymaga wyjaśnienia i dokumentacji.
Wyścigi lekkości na orbicie
17

Nie. Nie ma naturalnego sposobu .

W rzeczywistości jedną z motywacji stojących za silnym typowaniem enum classw C ++ 11 jest zapobieganie ich cichej konwersji do int.

iammilind
źródło
Spójrz na odpowiedź Khurshida Normuradova. Przychodzi w „naturalny sposób” i jest bardzo podobny do zamierzeń w „Języku programowania C ++ (wydanie 4-te)”. Nie dzieje się to w sposób „automatyczny” i to dobrze.
PapaAtHome
@PapaAtHome Nie rozumiem korzyści płynących z tego w porównaniu z static_cast. Niewiele zmian w pisaniu lub czystości kodu. Co tu jest naturalne? Funkcja zwracająca wartość?
Atul Kumar
1
@ user2876962 Zaletą dla mnie jest to, że nie jest automatyczny ani „cichy”, jak to ujął Iammilind. Zapobiega to trudnościom w znalezieniu błędów. Nadal możesz wykonać rzut, ale musisz o tym pomyśleć. W ten sposób wiesz, co robisz. Dla mnie jest to część nawyku „bezpiecznego kodowania”. Wolę, aby żadne konwersje nie były wykonywane automatycznie, ponieważ istnieje szansa, że ​​może to spowodować błąd. Sporo zmian w C ++ 11 związanych z systemem typów należy do tej kategorii, jeśli o mnie chodzi.
PapaAtHome
17

W innych odpowiedziach podano przyczynę braku niejawnej konwersji (z założenia).

Osobiście używam jednoargumentowego operator+do konwersji z klas wyliczeniowych do ich bazowego typu:

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

Co daje dość niewielkie „narzuty związane z pisaniem”:

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

Gdzie faktycznie używam makra do tworzenia wyliczeń i funkcji operatora w jednym ujęciu.

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

Mam nadzieję, że to pomoże tobie lub komuś innemu

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
źródło
33
Nazywa się to „punningiem typu” i chociaż jest obsługiwane przez niektóre kompilatory, nie jest przenośne, ponieważ standard C ++ mówi, że po ustawieniu un.ijest to „aktywny element członkowski” i można odczytać tylko aktywnego członka unii.
Jonathan Wakely
6
@JonathanWakely Masz rację techniczną, ale nigdy nie widziałem kompilatora, w którym to nie działa niezawodnie. Takie rzeczy, anonimowe związki i raz #pragma są standardami defacto.
BigSandwich
5
Po co używać czegoś, czego standard wyraźnie zabrania, skoro wystarczy zwykła obsada? To jest po prostu złe.
Paul Groke,
1
Technicznie poprawne czy nie, dla mnie jest o wiele bardziej czytelne niż inne rozwiązania tutaj znalezione. Co ważniejsze dla mnie, można go wykorzystać do rozwiązania nie tylko serializacji, ale także deserializacji klasy wyliczeniowej z łatwością i czytelnym formatem.
Marcin Waśniowski
6
Absolutnie rozpaczam, że są ludzie, którzy uważają to niechlujne, niezdefiniowane zachowanie za „o wiele bardziej czytelne” niż proste static_cast.
underscore_d
13

Krótka odpowiedź brzmi: nie możesz, jak wskazują powyższe posty. Ale w moim przypadku po prostu nie chciałem zaśmiecać przestrzeni nazw, ale nadal mieć niejawne konwersje, więc po prostu zrobiłem:

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

Sortowanie przestrzeni nazw dodaje warstwę bezpieczeństwa typu, podczas gdy nie muszę statycznie rzutować żadnych wartości wyliczeniowych na typ bazowy.

przesilenie 333
źródło
3
Nie dodaje żadnego bezpieczeństwa typu (w rzeczywistości właśnie usunąłeś bezpieczeństwo typu) - dodaje tylko zakres nazwy.
Wyścigi lekkości na orbicie
@LightnessRacesinOrbit tak, zgadzam się. Kłamałem. Technicznie rzecz biorąc, dokładnie rzecz biorąc, typ znajduje się tuż pod przestrzenią nazw / zakresem i w pełni kwalifikuje się do Foo::Foo. Dostęp do elementów członkowskich można uzyskać jako Foo::bari Foo::bazmożna je niejawnie rzutować (a więc nie ma dużego bezpieczeństwa typu). Prawdopodobnie lepiej jest prawie zawsze używać klas wyliczeniowych, zwłaszcza jeśli rozpoczynasz nowy projekt.
przesilenie
6

Wydaje się to niemożliwe w przypadku tubylców enum class , ale prawdopodobnie można kpić enum classz class:

W tym przypadku,

enum class b
{
    B1,
    B2
};

byłoby równoważne z:

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

W większości jest to odpowiednik oryginału enum class. Możesz bezpośrednio wrócićb::B1 for w funkcji z typem zwracanym b. Możesz switch casez tym zrobić itp.

W duchu tego przykładu możesz użyć szablonów (prawdopodobnie razem z innymi rzeczami) do uogólnienia i mockowania dowolnego możliwego obiektu zdefiniowanego przez enum classskładnię.

Colliot
źródło
ale B1 i B2 muszą być zdefiniowane poza klasą ... lub jest to bezużyteczne dla case - header.h <- class b - main.cpp <---- myvector.push_back (B1)
Fl0
Czy nie powinno to być „static constexpr b” zamiast „static constexpr int”? W przeciwnym razie b :: B1 to po prostu int bez żadnego zabezpieczenia typów.
Some Guy
4

Jak wielu powiedziało, nie ma sposobu na automatyczną konwersję bez dodawania narzutów i zbyt dużej złożoności, ale możesz trochę zmniejszyć pisanie i sprawić, by wyglądało lepiej, używając lambd, jeśli niektóre rzutowanie będą używane trochę w scenariuszu. Dodałoby to trochę wywołania narzutu funkcji, ale uczyniłoby kod bardziej czytelnym w porównaniu z długimi ciągami static_cast, jak widać poniżej. Może to nie być przydatne w całym projekcie, ale tylko w klasie.

#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
źródło
2

Komisja C ++ zrobiła jeden krok naprzód (określanie zakresu wyliczeń z globalnej przestrzeni nazw) i pięćdziesiąt kroków wstecz (brak zaniku typu wyliczenia na liczbę całkowitą). Niestetyenum class po prostu nie nadaje się do użytku, jeśli potrzebujesz wartości wyliczenia w jakikolwiek nie symboliczny sposób.

Najlepszym rozwiązaniem jest nieużywanie go w ogóle, a zamiast tego samodzielne określanie zakresu wyliczenia przy użyciu przestrzeni nazw lub struktury. W tym celu są wymienne. Będziesz musiał wpisać trochę więcej, odwołując się do samego typu wyliczenia, ale prawdopodobnie nie będzie to często.

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
źródło