Flag-Enums mit C++



  • Gerade habe ich mal wieder Bitflags gebraucht. Es ist ja bekannt, daß man für ein enum die Bitoperatoren überladen kann, damit es sich wie typisierte Bitflags benutzen läßt; gerade fiel mir eine Möglichkeit ein, diese Überladungen in einer Basisklasse zu verstecken:

    struct Gemüsesuppe : Flags<Gemüsesuppe>
    {
        constexpr Gemüsesuppe(void) noexcept {}
        constexpr Gemüsesuppe(Flag flag) noexcept : base(flag) {}
    
        static constexpr auto Sellerie = Flag(1);
        static constexpr auto Karotten = Flag(2);
        static constexpr auto Porree = Flag(4);
        static constexpr auto Kartoffeln = Flag(8);
    };
    

    Flags<> sieht dabei aus wie folgt:

    template <typename FlagsT, typename UnderlyingTypeT = unsigned>
        struct Flags
    {
        using base = Flags;
    
        enum Flag : UnderlyingTypeT { None = 0 };
    
        Flag value;
    
        constexpr Flags(void) noexcept { }
        constexpr Flags(Flag flag) noexcept : value(flag) { }
    
        friend constexpr FlagsT operator |(Flag lhs, Flag rhs) noexcept { ... }
        ...
        friend constexpr bool operator ==(FlagsT lhs, FlagsT rhs) noexcept { ... }
        ...
        friend constexpr FlagsT operator |(FlagsT lhs, FlagsT rhs) noexcept { ... }
        ...
    
        constexpr FlagsT& operator =(Flag rhs) noexcept { ... }
        constexpr FlagsT& operator |=(Flag rhs) noexcept { ... }
        ...
    
        constexpr bool haveFlag(Flag flag) const noexcept { ... }
        constexpr bool haveAny(FlagsT flags) const noexcept { ... }
        constexpr bool haveAll(FlagsT flags) const noexcept { ... }
    };
    

    Ganz so schön wie in Delphi ist es noch nicht:

    type
      TGemüsesuppe = set of (Sellerie, Karotten, Porree, Kartoffeln);
    

    Es sieht eher aus wie in C#.

    Mir gefällts im Prinzip, aber ich bin noch nicht sicher, ob das bißchen Benutzungssicherheit den Aufwand wert ist. Und vielleicht geht es ja noch besser? Was meint ihr dazu?



  • Hallo audacia

    Sieht ganz gut aus. Aber eine Frage bleibt mir: wie gedenkst du haveAll zu implementieren? Ein einfaches == ~UnderlyingTypeT{} tut es ja nicht, wie du an deiner Klasse Gemüsesuppe erkennen kannst.
    Und lass Gemüsesuppe doch die Konstruktoren von der Basisklasse erben, statt sie nochmals identisch neu zu implementieren.

    LG



  • Fytch schrieb:

    wie gedenkst du haveAll zu implementieren?

    constexpr bool haveAll(FlagsT flags) const noexcept { return (value & flags.value) == flags.value; }
    

    Vermutlich macht das aber nicht, was du erwartet hast.

    Fytch schrieb:

    Und lass Gemüsesuppe doch die Konstruktoren von der Basisklasse erben, statt sie nochmals identisch neu zu implementieren.

    Daß man Konstruktoren erben kann, war mir neu. Danke für den Hinweis!



  • Ein Problem beim Ersetzen primitiver Typen durch benutzerdefinierte Typen ist, daß es sehr leicht ist, odr-usage zu verursachen, was wiederum beim Deklarieren von Konstanten hinderlich ist, denn dann muß man sie auch definieren. Mein erster Versuch hatte kein enum Flag : UnderlyingTypeT { } , sondern Flag war auch ein struct ; damit erzeugte Clang Linkerfehler für eine analoge Definition und eine übliche Verwendung von Gemüsesuppe , obwohl ich alle Operatoren als nicht-Memberfunktionen mit by-value-Argumenten definiert habe.

    Ich bin nicht ganz sicher, warum das der Fall ist. Die formale Definition von odr-usage auf cppreference.com ist:

    cppreference.com schrieb:

    1. a variable x in a potentially-evaluated expression ex is odr-used unless both of the following are true:
    • applying lvalue-to-rvalue conversion to x yields a constant expression that doesn't invoke non-trivial functions
    struct S { static const int x = 1; };
    int f() { return S::x; } // does not odr-use S::x
    
    • either x is not an object (that is, x is a reference) or, if x is an object, it is one of the potential results of a larger expression e , where that larger expression is either a discarded-value expression or an lvalue-to-rvalue conversion
    struct S { static const int x = 1; };
    void f() { &S::x; } // discarded-value expression does not odr-use S::x
    
    1. this is odr-used if it appears as a potentially-evaluated expression (including the implicit this in a non-static member function call expression)

    Was ich spezifisch nicht verstehe, ist der Satz " x is not an object (that is, x is a reference)". Meint "object" einen benutzerdefinierten Typen? Falls ja, warum ist es dann eine Referenz? Gibt es für benutzerdefinierte Typen andere Regeln, ab wann sie in einem Ausdruck zu einer Referenz werden?

    (Dieses Problem wird wohl in C++17 vermieden, indem statische constexpr -Members implizit inline sind. Die odr-usage verstehe ich trotzdem nicht.)


  • Mod

    Was ich spezifisch nicht verstehe, ist der Satz " x is not an object (that is, x is a reference)". Meint "object" einen benutzerdefinierten Typen?

    object heißt Objekt. Da gibt es wenig Spielraum.

    (Dieses Problem wird wohl in C++17 vermieden, indem statische constexpr -Members implizit inline sind.

    Und woran hast du das abgeleitet? Aber ja, inline wäre adäquat.

    In deinem Fall wird es wohl so gewesen sein, dass bspw. eine Referenz an das Flag gebunden werden sollte. Da Enumeratoren prvalues sind, werden sie einfach kopiert, was also keinen odr-use darstellt (btw. zitiere bitte C++14 oder den WP).



  • Arcoth schrieb:

    object heißt Objekt. Da gibt es wenig Spielraum.

    Ach. 🤡

    Ich bin nicht so vertraut mit standardese und denke bei Objekten an Instanzen von Klassen. Was ist nun ein Objekt? Die Definition unter http://eel.is/c++draft/intro.object lese ich so, daß ein Objekt die Laufzeitrepräsentation einer Instanz eines Typen ist, d.h. nach int i; ist auch i ein Objekt. Ist das ungefähr richtig?

    Und warum soll ein Objekt dann eine Referenz sein? Oder ist der zitierte Halbsatz mit der Referenz einfach falsch?

    Arcoth schrieb:

    Und woran hast du das abgeleitet?

    Das habe ich unter http://en.cppreference.com/w/cpp/language/constexpr gelesen:

    cppreference.com schrieb:

    A constexpr specifier used in a function [or static member variable (since C++17)] declaration implies inline.

    Arcoth schrieb:

    In deinem Fall wird es wohl so gewesen sein, dass bspw. eine Referenz an das Flag gebunden werden sollte.

    Gut, aber ich verstehe nicht, warum. Ich habe weder Memberfunktionen von Flag definiert, noch hatten meine Operatorüberladungen by-ref-Argumente.

    Arcoth schrieb:

    Da Enumeratoren prvalues sind

    Wo wird denn definiert, wie und wann sich Instanzen strukturierter benutzerdefinierter Typen ( struct , class , union ) und Instanzen elementarer Typen (worunter dann wohl auch Enumeratoren fallen?) in ihrem expression type unterscheiden? Warum ist meine struct -Instanz kein prvalue?


  • Mod

    audacia schrieb:

    Arcoth schrieb:

    object heißt Objekt. Da gibt es wenig Spielraum.

    Ach. 🤡

    Ich bin nicht so vertraut mit standardese und denke bei Objekten an Instanzen von Klassen. Was ist nun ein Objekt? Die Definition unter http://eel.is/c++draft/intro.object lese ich so, daß ein Objekt die Laufzeitrepräsentation einer Instanz eines Typen ist, d.h. nach int i; ist auch i ein Objekt. Ist das ungefähr richtig?

    Das ist korrekt.

    Und warum soll ein Objekt dann eine Referenz sein?

    Das sind zwei völlig unterschiedliche Dinge. Und der von dir zitierte bullet point macht das auch deutlich: Eine Variable ist entweder ein Objekt oder eine Referenz, aber natürlich nicht beides.

    Arcoth schrieb:

    Und woran hast du das abgeleitet?

    Das habe ich unter http://en.cppreference.com/w/cpp/language/constexpr gelesen:

    cppreference.com schrieb:

    A constexpr specifier used in a function [or static member variable (since C++17)] declaration implies inline.

    Korrekt. Ich hatte das constexpr überlesen.

    Arcoth schrieb:

    In deinem Fall wird es wohl so gewesen sein, dass bspw. eine Referenz an das Flag gebunden werden sollte.

    Gut, aber ich verstehe nicht, warum. Ich habe weder Memberfunktionen von Flag definiert, noch hatten meine Operatorüberladungen by-ref-Argumente.

    Das hat doch damit nichts zu tun. Hast du ein Flag an irgendeine Referenz gebunden (bpsw. auch mittels perfect forwarding), oder nicht? Zeig doch mal den Code, der die ODR verletzt haben soll.

    Arcoth schrieb:

    Da Enumeratoren prvalues sind

    Wo wird denn definiert, wie und wann sich Instanzen strukturierter benutzerdefinierter Typen ( struct , class , union ) und Instanzen elementarer Typen (worunter dann wohl auch Enumeratoren fallen?) in ihrem expression type unterscheiden?

    *Value category.

    Warum ist meine struct -Instanz kein prvalue?

    Weil es ein Objekt benennt. Ein Enumerator ist kein Objekt, sondern mehr wie ein Template-Parameter; nur ein "Wert".



  • Danke für deine Antworten und Hinweise, das ist sehr hilfreich.

    Arcoth schrieb:

    Und warum soll ein Objekt dann eine Referenz sein?

    Das sind zwei völlig unterschiedliche Dinge. Und der von dir zitierte bullet point macht das auch deutlich: Eine Variable ist entweder ein Objekt oder eine Referenz, aber natürlich nicht beides.

    Hoppla, richtig. Da hatte ich das "not" überlesen 🙄

    Arcoth schrieb:

    Das hat doch damit nichts zu tun. Hast du ein Flag an irgendeine Referenz gebunden (bpsw. auch mittels perfect forwarding), oder nicht? Zeig doch mal den Code, der die ODR verletzt haben soll.

    Hier:

    #include <cstdio>
    
    struct Bar
    {
        struct Foo { unsigned i; };
    
        static constexpr Foo value = Foo { 42 };
    
        friend Foo operator |(Foo lhs, Foo rhs) { return Foo { lhs.i | rhs.i }; }
    };
    
    // mit der folgenden Zeile gehts:
    // constexpr Bar::Foo Bar::value;
    
    int main(void)
    {
        Bar::Foo v = Bar::value | Bar::value;
        std::printf("%u\n", v.i);
    }
    

    Das Problem habe ich übrigens nur mit Clang (getestet mit v3.8, glaube ich); g++ und VC++ erzeugen keine Linkerfehler.


  • Mod

    Ich hatte Referenzen erwähnt, weil ich das als Hauptursache in Erinnerung hatte, aber tatsächlich sind auch einfache Kopien odr-uses; Ich frage mich, ob man eine Erweiterung für trivial kopierbare Klassen einführen könnte.

    Clang 3.8 hat inline Variablen nicht implementiert, aber 3.9 schon. GCC hat sie ebenfalls implementiert.



  • Nachdem ich jetzt verstanden zu haben glaube, was Objekte sind, kann ich auch die obige Passage aus dem Draft und ihre Konsequenzen bzgl. odr-usage nachvollziehen. Dann hatte man eigentlich ohne inline variables überhaupt keine Chance, auf standardkonforme Weise mit constexpr eine Instanz eines benutzerdefinierten Typen zu deklarieren und zu benutzen, ohne auch eine Definition anzugeben? Außerdem kommt mir sinnlos vor, daß es sich formal auch um odr-usage handelt, wenn der ganze Auswertungspfad constexpr ist. Aber egal, spielt ja dann bald keine Rolle mehr.

    Edit: Nebensatz vergessen

    Edit 2: Was aber ist mit der lvalue-to-rvalue conversion, die auch für Objekte von odr-usage ausgenommen ist? Ohne die jetzt nachgelesen und formal verstanden zu haben: sollte die nicht für eine einfache Kopie vorgenommen werden, so daß eine Kopie doch kein odr-use wäre?

    Das ist jedenfalls auch meine Erfahrung mit Clang; wenn ich komplexere Typen als static constexpr -Member deklariere, reicht es, vor ihrer Benutzung eine lokale constexpr -Kopie zu machen, um die Linkerfehler zu vermeiden.


  • Mod

    audacia schrieb:

    Was aber ist mit der lvalue-to-rvalue conversion, die auch für Objekte von odr-usage ausgenommen ist? Ohne die jetzt nachgelesen und formal verstanden zu haben: sollte die nicht für eine einfache Kopie vorgenommen werden, so daß eine Kopie doch kein odr-use wäre?

    l-t-r Konvertierungen werden i.d.R. nicht auf Klassen-Objekte angewandt, sondern Skalare. Sie stellen quasi einen Speicherzugriff dar.

    audacia schrieb:

    Das ist jedenfalls auch meine Erfahrung mit Clang; wenn ich komplexere Typen als static constexpr -Member deklariere, reicht es, vor ihrer Benutzung eine lokale constexpr -Kopie zu machen, um die Linkerfehler zu vermeiden.

    Das löst doch das Problem nicht: als Initializer der lokalen constexpr Variable wird der Member trotzdem odr-used. Zeig mehr Code.



  • Arcoth schrieb:

    l-t-r Konvertierungen werden i.d.R. nicht auf Klassen-Objekte angewandt, sondern Skalare. Sie stellen quasi einen Speicherzugriff dar.

    Das geht für mich nicht aus dem Text hervor.

    Versuchen wir mal systematisch, herauszufinden, ob

    Bar::Foo v = Bar::value;
    

    ein odr-use von Bar::value ist:

    http://eel.is/c++draft/basic.def.odr#3
    "A variable x whose name appears as a potentially-evaluated expression ex is odr-used by ex unless" (offensichtlich irrelevante Punkte auf allen Ebenen weggelassen):

    • "applying the lvalue-to-rvalue conversion to x yields a constant expression":
      Unter http://eel.is/c++draft/conv.lval deutet nichts darauf hin, daß lvalue-to-rvalue conversion für "objects of class type" nicht anwendbar sein soll. (Zur Klärung der Frage, ob "the value contained in the referenced object ... accessed" wird, wird wieder auf odr-usage verwiesen; ich nehme also an, diese Frage spielt hier keine Rolle, sonst wird die Definition zyklisch.)
      Dann unter http://eel.is/c++draft/expr.const nachsehen, ob wir eine constant expression haben:
      "An entity is a permitted result of a constant expression if it is an object with static storage duration that is ... not a temporary object". OK.
      "A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression, or a prvalue core constant expression whose value satisfies the following constraints:"

    • "if the value is an object of class type, each non-static data member of reference type refers to an entity that is a permitted result of a constant expression": OK, es gibt gibt keine "non-static data members of reference type"

    • "if the value is an object of class or array type, each subobject satisfies these constraints for the value": auch OK

    Also noch nachweisen, daß wir eine core constant expression haben:
    "An expression e is a core constant expression unless the evaluation of e , following the rules of the abstract machine, would evaluate one of the following expressions:"

    • "this, except in a constexpr function or a constexpr constructor that is being evaluated as part of e": OK

    • "an invocation of a function other than a constexpr constructor for a literal class, a constexpr function, or an implicit invocation of a trivial destructor ([class.dtor])": OK, alles ist constexpr

    • "an lvalue-to-rvalue conversion unless it is applied to"

    • "a non-volatile glvalue that refers to a non-volatile object defined with constexpr, or that refers to a non-mutable subobject of such an object": OK, gegeben

    • "a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of e": hier egal, aber das dürfte dann den Rückgabewert einer constexpr -Funktion betreffen

    • "modification of an object ([expr.ass], [expr.post.incr], [expr.pre.incr]) unless it is applied to a non-volatile lvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of e": OK, trifft nicht zu

    • "...that does not invoke any non-trivial functions": OK

    • "and ex is an element of the set of potential results of an expression e " where the lvalue-to-rvalue conversion is applied to e ": das soll wohl heißen: x darf nur in einer lvalue-to-rvalue conversion vorkommen.

    Vielleicht habe ich bei der lvalue-to-rvalue conversion irgendwas nicht verstanden, aber das sieht doch so aus, als wäre obiger Fall keine odr-usage, oder?


  • Mod

    audacia schrieb:

    Arcoth schrieb:

    l-t-r Konvertierungen werden i.d.R. nicht auf Klassen-Objekte angewandt, sondern Skalare. Sie stellen quasi einen Speicherzugriff dar.

    Das geht für mich nicht aus dem Text hervor.

    Das ergibt sich, wenn du beachtest, unter welchen Umständen eine l-t-r-Konvertierung durchgeführt wird. Dafür gibt es ja keine explizite Syntax, sondern sie wird durchgeführt, wenn 1. der entsprechende Kontext (implizit) ein r-Value benötigt, aber ein l-Value dasteht, und 2. die Konvertierung in diesem Kontext überhaupt auch zulässig ist.
    Nun gibt es überhaupt nur sehr wenige Kontexte, die r-Values von Klassenobjekten verlangen, etwa:
    - Initialisierung einer r-Value-Referenz, oder
    - Aufruf einer Memberfunktion mit &&-Qualifier
    In beiden Fällen wird diese Konvertierung aber unterdrückt.

    In

    Bar::Foo v = Bar::value;
    

    ist das zu initialisierende Objekt ein Klassenobjekt. Wie werden Klassenobjekte i.d.R. initialisiert? Durch Aufruf eines passenden Konstruktors. In diesem speziellen Fall der Copy-Konstruktor. Dessen Signatur:

    Bar::Foo(const Bar::Foo&);
    

    Hier muss zum Aufruf nur eine Referenz initialisiert werden, was keine l-t-r-Konvertierung des Arguments erfordert.

    Kleiner Nebengedanke: die ganze Logik, die hinter der Movesemantik steht, kann überhaupt nur funktionieren, weil eine entsprechende l-t-r-Konvertierung für Klassenobjekte nie stattfindet. Der Grund, warum wir ohne Weiteres aus pr-Values Resourcen entfernen dürfen, ist ja der, dass die entsprechenden Objekte aliasfrei sind und somit kein späterer Zugriff auf sie vor ihrer Zerstörung möglich ist.



  • camper schrieb:

    Das ergibt sich, wenn du beachtest, unter welchen Umständen eine l-t-r-Konvertierung durchgeführt wird. Dafür gibt es ja keine explizite Syntax, sondern sie wird durchgeführt, wenn 1. der entsprechende Kontext (implizit) ein r-Value benötigt, aber ein l-Value dasteht, und 2. die Konvertierung in diesem Kontext überhaupt auch zulässig ist.
    Nun gibt es überhaupt nur sehr wenige Kontexte, die r-Values von Klassenobjekten verlangen, etwa:
    - Initialisierung einer r-Value-Referenz, oder
    - Aufruf einer Memberfunktion mit &&-Qualifier
    In beiden Fällen wird diese Konvertierung aber unterdrückt.

    Das stimmt, um die Frage, ob überhaupt eine lvalue-to-rvalue conversion vorgenommen wird, habe ich mich gedrückt 🙂 Danke für deine Aufklärung.

    camper schrieb:

    Kleiner Nebengedanke: die ganze Logik, die hinter der Movesemantik steht, kann überhaupt nur funktionieren, weil eine entsprechende l-t-r-Konvertierung für Klassenobjekte nie stattfindet. Der Grund, warum wir ohne Weiteres aus pr-Values Resourcen entfernen dürfen, ist ja der, dass die entsprechenden Objekte aliasfrei sind und somit kein späterer Zugriff auf sie vor ihrer Zerstörung möglich ist.

    Nur leider gibt es std::move() bzw. einen erlaubten Cast nach T&& , so daß eine rvalue-Referenz eben nicht immer aliasfrei ist. Sonst hätte man den Move-Konstruktor an einen Destruktor koppeln können und müßte also gar nicht über Zombieobjekte nachdenken.

    Eigentlich schade, daß man auf die mandatory copy elision nicht schon früher gekommen ist; vielleicht hätte es dann auch eine destruktive Movesemantik getan. Ich hab das gerade mal gegooglet und hier etwas gefunden, und das führt als Argument gegen destruktive Movesemantik u. a. an, daß man sie nur auf dynamisch verwaltete Objekte anwenden könnte. Dieses Argument wird durch mandatory copy elision ja größtenteils hinfällig, oder?


  • Mod

    audacia schrieb:

    camper schrieb:

    Kleiner Nebengedanke: die ganze Logik, die hinter der Movesemantik steht, kann überhaupt nur funktionieren, weil eine entsprechende l-t-r-Konvertierung für Klassenobjekte nie stattfindet. Der Grund, warum wir ohne Weiteres aus pr-Values Resourcen entfernen dürfen, ist ja der, dass die entsprechenden Objekte aliasfrei sind und somit kein späterer Zugriff auf sie vor ihrer Zerstörung möglich ist.

    Nur leider gibt es std::move() bzw. einen erlaubten Cast nach T&& , so daß eine rvalue-Referenz eben nicht immer aliasfrei ist. Sonst hätte man den Move-Konstruktor an einen Destruktor koppeln können und müßte also gar nicht über Zombieobjekte nachdenken.

    pr-Value != r-Value . rValue-Referenzen (mithin: x-Values) sind ja der zweite Schritt. Zunächst hat man nur geschaut, was mit den "richtigen" rValues in C++03 los ist, und wie man deren Behandlung verbessern kann und welche semantischen Beschränkungen gelten. Hat man einmal rValue-Referenzen, kann man im Anschluss darüber nachdenken, was ein entsprechender Cast bedeuten soll. Schreibe ich

    move(foo)
    

    drücke ich ja damit aus, dass foo gerade eigentlich nicht zu Moven wäre, wir den Compiler aber direkt anweisen, so zu tun, als ob ein rValue da wäre. Damit ist es nat. wie mit jedem anderen Cast (=Umgehung des Typsystems) auch: der Programmierer hat dafür zu sorgen, dass das Sinn macht.



  • Ich bin jetzt nach ein paar Iterationen bei folgendem Design angekommen:

    template <typename FlagsT, typename UnderlyingTypeT = unsigned>
        struct DefineFlags
    {
        enum class Flags : UnderlyingTypeT { None = 0 };
        using Flag = Flags;
    
        friend constexpr Flags operator |(Flags lhs, Flags rhs) noexcept { return Flags(UnderlyingTypeT(lhs) | UnderlyingTypeT(rhs)); }
        friend constexpr Flags operator &(Flags lhs, Flags rhs) noexcept { return Flags(UnderlyingTypeT(lhs) & UnderlyingTypeT(rhs)); }
        friend constexpr Flags operator ^(Flags lhs, Flags rhs) noexcept { return Flags(UnderlyingTypeT(lhs) ^ UnderlyingTypeT(rhs)); }
        friend constexpr Flags operator ~(Flags arg) noexcept { return Flags(~UnderlyingTypeT(arg)); }
        friend constexpr Flags& operator |=(Flags& lhs, Flags rhs) noexcept { lhs = lhs | rhs; return lhs; }
        friend constexpr Flags& operator &=(Flags& lhs, Flags rhs) noexcept { lhs = lhs & rhs; return lhs; }
        friend constexpr Flags& operator ^=(Flags& lhs, Flags rhs) noexcept { lhs = lhs ^ rhs; return lhs; }
        friend constexpr bool hasFlag(Flags flags, Flag flag) noexcept { return (UnderlyingTypeT(flags) & UnderlyingTypeT(flag)) != 0; }
        friend constexpr bool hasAny(Flags flags, Flag flag) noexcept { return (UnderlyingTypeT(flags) & UnderlyingTypeT(flag)) != 0; }
        friend constexpr bool hasAll(Flags flags, Flag flag) noexcept { return Flags(UnderlyingTypeT(flags) & UnderlyingTypeT(flag)) == flag; }
    };
    

    Benutzung:

    struct Gemüse : DefineFlags<Gemüse>
    {
        static constexpr auto Sellerie = Flag(1); 
        static constexpr auto Karotten = Flag(2); 
        static constexpr auto Porree = Flag(4); 
        static constexpr auto Kartoffeln = Flag(8); 
        static constexpr auto Spargel = Flag(16); 
    };
    using Gemüsesuppe = Gemüse::Flags;
    

    Das ist einerseits schöner, weil man jetzt Gemüse::Sellerie schreibt und nicht Gemüsesuppe::Sellerie , andererseits ist Gemüsesuppe jetzt einfach ein Enumerator, womit die o.g. odr-use-Problematik wegfällt. Außerdem kann ich Gemüsesuppe jetzt auch in switch -Statements verwenden:

    bool inSpeisekarte(Gemüsesuppe suppe)
    {
        switch(suppe)
        {
        case Gemüse::Spargel: // "...und als Vorspeise nehm ich die smackhafte Spargelcremesuppe bitte!"
            return true; // "Sehr gern."
        case Gemüse::Karotten:
        case Gemüse::Kartoffeln | Gemüse::Porree:
            return true;
        default:
            return false;
        }
    }
    

    Edit: kleine Korrekturen


Anmelden zum Antworten