Wie konvertiere ich stark typisierte Enum automatisch in int?

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

Das a::LOCAL_Aist es, was die stark typisierte Aufzählung zu erreichen versucht, aber es gibt einen kleinen Unterschied: Normale Aufzählungen können in Ganzzahlentypen konvertiert werden, während stark typisierte Aufzählungen dies nicht ohne Umwandlung tun können.

Gibt es also eine Möglichkeit, einen stark typisierten Aufzählungswert in einen ganzzahligen Typ ohne Umwandlung umzuwandeln? Wenn ja, wie?

BЈовић
quelle

Antworten:

134

Stark typisierte Enums, die darauf abzielen, mehrere Probleme zu lösen und nicht nur das in Ihrer Frage erwähnte Scoping-Problem:

  1. Bieten Sie Typensicherheit und vermeiden Sie so die implizite Konvertierung in eine Ganzzahl durch integrale Heraufstufung.
  2. Geben Sie die zugrunde liegenden Typen an.
  3. Sorgen Sie für ein starkes Scoping.

Daher ist es unmöglich, eine stark typisierte Aufzählung implizit in Ganzzahlen oder sogar in den zugrunde liegenden Typ umzuwandeln - das ist die Idee. Sie müssen also verwenden static_cast, um die Konvertierung explizit zu machen.

Wenn Ihr einziges Problem das Scoping ist und Sie wirklich eine implizite Heraufstufung in Ganzzahlen wünschen, sollten Sie besser nicht stark typisierte Aufzählungen mit dem Umfang der Struktur verwenden, in der sie deklariert sind.

LF
quelle
2
Dies ist ein weiteres seltsames Beispiel für "Wir wissen besser, was Sie tun möchten" von C ++ - Erstellern. Herkömmliche Aufzählungen (im alten Stil) hatten unzählige Vorteile wie die implizite Konvertierung in Indizes, die nahtlose Verwendung bitweiser Operationen usw. Die Aufzählungen im neuen Stil haben eine wirklich großartige Scoping-Sache hinzugefügt, aber ... Sie können nicht genau diese Sache verwenden (auch nicht mit expliziten zugrunde liegende Typenspezifikation!). Jetzt müssen Sie entweder alte Aufzählungen mit Tricks wie dem Einfügen in eine Struktur verwenden oder hässlichste Problemumgehungen für neue Aufzählungen erstellen, z. B. einen eigenen Wrapper um std :: vector erstellen, um diese CAST-Sache zu überwinden. Keine Kommentare
Avtomaton
152

Wie andere gesagt haben, kann es keine implizite Konvertierung geben, und das ist beabsichtigt.

Wenn Sie möchten, können Sie vermeiden, dass der zugrunde liegende Typ in der Besetzung angegeben werden muss.

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
quelle
74

Eine C ++ 14-Version der Antwort von R. Martinho Fernandes wäre:

#include <type_traits>

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

Wie bei der vorherigen Antwort funktioniert dies mit jeder Art von Aufzählung und zugrunde liegendem Typ. Ich habe das noexceptSchlüsselwort hinzugefügt, da es niemals eine Ausnahme auslösen wird.


Update
Dies erscheint auch in Effective Modern C ++ von Scott Meyers . Siehe Punkt 10 (er ist auf den letzten Seiten des Artikels in meiner Kopie des Buches aufgeführt).

Klassenskelett
quelle
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
quelle
3
Dies reduziert weder die Eingabe noch macht es den Code sauberer und hat den Nebeneffekt, dass es schwieriger wird, solche impliziten Konvertierungen in großen Projekten zu finden. Static_cast wäre einfacher, projektweit zu suchen als diese Konstrukte.
Atul Kumar
3
@AtulKumar Wie ist die Suche nach static_cast einfacher als die Suche nach to_enum?
Johann Gerell
1
Diese Antwort bedarf einiger Erläuterungen und Dokumentationen.
Leichtigkeitsrennen im Orbit
17

Nein, es gibt keinen natürlichen Weg .

Tatsächlich besteht eine der Beweggründe für die starke Eingabe enum classin C ++ 11 darin, die stille Konvertierung in C ++ 11 zu verhindern int.

iammilind
quelle
Schauen Sie sich die Antwort von Khurshid Normuradov an. Es kommt auf "natürliche Weise" und entspricht weitgehend der Absicht in "The C ++ Programming Language (4th ed.)". Es kommt nicht auf "automatische Weise", und das ist gut so.
PapaAtHome
@PapaAtHome Ich verstehe den Nutzen davon gegenüber static_cast nicht. Nicht viel Änderung in der Eingabe oder Code-Sauberkeit. Was ist hier natürlich? Eine Funktion, die einen Wert zurückgibt?
Atul Kumar
1
@ user2876962 Der Vorteil für mich ist, dass es nicht automatisch oder "leise" ist, wie Iammilind es ausdrückt. Das verhindert, dass es schwierig ist, Fehler zu finden. Du kannst immer noch eine Besetzung machen, aber du bist gezwungen darüber nachzudenken. Auf diese Weise wissen Sie, was Sie tun. Für mich ist es Teil einer "sicheren Codierung". Ich bevorzuge, dass keine Konvertierungen nicht automatisch durchgeführt werden, da die Wahrscheinlichkeit gering ist, dass ein Fehler auftritt. Einige Änderungen in C ++ 11 in Bezug auf das Typsystem fallen in diese Kategorie, wenn Sie mich fragen.
PapaAtHome
17

Der Grund für das Fehlen einer impliziten Konvertierung (beabsichtigt) wurde in anderen Antworten angegeben.

Ich persönlich verwende unary operator+für die Konvertierung von Aufzählungsklassen in den zugrunde liegenden Typ:

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

Was ziemlich wenig "Tippaufwand" gibt:

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

Wo ich tatsächlich ein Makro verwende, um Enums und die Operatorfunktionen auf einmal zu erstellen.

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

Hoffe das hilft dir oder jemand anderem

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
quelle
33
Dies wird als "Typ Punning" bezeichnet und ist zwar von einigen Compilern unterstützt, aber nicht portierbar, da der C ++ - Standard besagt, dass nach dem Festlegen un.idas "aktive Mitglied" ist und Sie nur das aktive Mitglied einer Gewerkschaft lesen können.
Jonathan Wakely
6
@ JonathanWakely Sie sind technisch korrekt, aber ich habe noch nie einen Compiler gesehen, bei dem dies nicht zuverlässig funktioniert. Solche Sachen, anonyme Gewerkschaften und #pragma sind einmal defacto-Standards.
BigSandwich
5
Warum etwas verwenden, das der Standard ausdrücklich verbietet, wenn eine einfache Besetzung ausreicht? Das ist einfach falsch.
Paul Groke
1
Technisch korrekt oder nicht, für mich ist es viel lesbarer als andere hier gefundene Lösungen. Und was für mich wichtiger ist, es kann verwendet werden, um nicht nur die Serialisierung, sondern auch die Deserialisierung der Enum-Klasse mit Leichtigkeit und lesbarem Format zu lösen.
Marcin Waśniowski
6
Ich bin absolut verzweifelt, dass es Leute gibt, die dieses unordentliche, undefinierte Verhalten als "viel lesbarer" als ein einfaches betrachten static_cast.
underscore_d
13

Eine kurze Antwort ist, dass Sie nicht wie oben beschrieben darauf hinweisen können. Aber für meinen Fall wollte ich den Namespace einfach nicht überladen, aber dennoch implizite Konvertierungen durchführen, also habe ich einfach Folgendes getan:

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

Die Art des Namespaces fügt eine Ebene der Typensicherheit hinzu, während ich dem zugrunde liegenden Typ keine Aufzählungswerte statisch umwandeln muss.

Sonnenwende333
quelle
3
Es wird keinerlei Typensicherheit hinzugefügt (tatsächlich haben Sie gerade die Typensicherheit entfernt) - es wird nur der Namensumfang hinzugefügt.
Leichtigkeitsrennen im Orbit
@ LightnessRacesinOrbit ja ich stimme zu. Ich habe gelogen. Technisch gesehen befindet sich der Typ genau unter einem Namensraum / Bereich und ist vollständig qualifiziert Foo::Foo. Auf Mitglieder kann als Foo::barund zugegriffen werden und sie Foo::bazkönnen implizit besetzt werden (und daher nicht viel Typensicherheit). Es ist wahrscheinlich besser, fast immer Enum-Klassen zu verwenden, insbesondere wenn Sie ein neues Projekt starten.
Sonnenwende333
6

Dies scheint mit dem Eingeborenen unmöglich enum class, aber wahrscheinlich können Sie ein enum classmit einem verspotten class:

In diesem Fall,

enum class b
{
    B1,
    B2
};

wäre gleichbedeutend mit:

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

Dies entspricht größtenteils dem Original enum class. Sie können b::B1in einer Funktion mit Rückgabetyp direkt für zurückgeben b. Sie können damit machen switch case, etc.

Im Sinne dieses Beispiels können Sie Vorlagen (möglicherweise zusammen mit anderen Dingen) verwenden, um jedes mögliche Objekt, das durch die enum classSyntax definiert wird, zu verallgemeinern und zu verspotten .

Colliot
quelle
aber B1 und B2 müssen außerhalb der Klasse definiert werden ... oder dies ist unbrauchbar für case - header.h <- Klasse b - main.cpp <---- myvector.push_back (B1)
Fl0
Sollte das nicht "static constexpr b" anstelle von "static constexpr int" sein? Andernfalls ist b :: B1 nur ein int ohne Typensicherheit.
Some Guy
4

Wie viele sagten, gibt es keine Möglichkeit, automatisch zu konvertieren, ohne Overhead und zu viel Komplexität hinzuzufügen, aber Sie können Ihre Eingabe ein wenig reduzieren und sie durch die Verwendung von Lambdas besser aussehen lassen, wenn einige Besetzungen in einem Szenario etwas häufig verwendet werden. Dies würde ein wenig Funktions-Overhead-Aufruf hinzufügen, aber den Code im Vergleich zu langen static_cast-Zeichenfolgen besser lesbar machen, wie unten zu sehen ist. Dies ist möglicherweise nicht projektweit nützlich, sondern nur klassenweit.

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

Das C ++ - Komitee machte einen Schritt vorwärts (Aufzählung von Aufzählungen aus dem globalen Namespace) und fünfzig Schritte zurück (kein Zerfall des Aufzählungstyps in eine Ganzzahl). Leider enum classist es einfach nicht verwendbar, wenn Sie den Wert der Aufzählung auf nicht symbolische Weise benötigen.

Die beste Lösung besteht darin, es überhaupt nicht zu verwenden und stattdessen die Aufzählung selbst mithilfe eines Namespace oder einer Struktur zu erfassen. Zu diesem Zweck sind sie austauschbar. Sie müssen ein wenig mehr eingeben, wenn Sie sich auf den Aufzählungstyp selbst beziehen, aber das wird wahrscheinlich nicht oft der Fall sein.

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
quelle