Czy klasa enum w języku C ++ może mieć metody?

145

Mam klasę enum z dwiema wartościami i chcę utworzyć metodę, która odbiera wartość i zwraca drugą. Chcę również zachować bezpieczeństwo typów (dlatego używam klasy wyliczeniowej zamiast wyliczeń).

http://www.cplusplus.com/doc/tutorial/other_data_types/ nie wspomina o metodach. Miałem jednak wrażenie, że każda klasa może mieć metody.

oktawski
źródło
4
Nie, nie może. Zobacz tutaj .
juanchopanza
@octavian Zanotuj moją odpowiedź i pomyśl o swoich przypadkach użycia, proszę!
πάντα ῥεῖ
@ πάνταῥεῖ masz całkowitą rację, przeczytałem enum, ale pomyślałem, że unia, zabił komentarz.
Eugen Constantin Dinca
@octavian Czy w ogóle pytasz o konkretny przypadek użycia, czy po prostu chciałeś potwierdzić ograniczenia standardów dla c ++ 11 enum class/struct ?
πάντα ῥεῖ
Miałem na myśli zastosowanie ... i to był fundamentalny problem
oktawski

Odpowiedzi:

118

Nie, nie mogą.

Rozumiem, że enum classczęść dotycząca silnie wpisanych wyliczeń w C ++ 11 może wydawać się sugerować, że twoje też enummają classcechy, ale tak nie jest. Zgaduję, że wybór słów kluczowych był inspirowany wzorcem, którego używaliśmy przed C ++ 11, aby uzyskać wyliczenia o określonym zakresie:

class Foo {
public:
  enum {BAR, BAZ};
};

Jednak to tylko składnia. Ponownie, enum classnie jest class.

Stefano Sanfilippo
źródło
88
W ## C ++ powiedziano mi, że „c ++ ma być jak najbardziej zagmatwany i przyjazny dla ekspertów” . Oczywiście to żart, ale masz pomysł :)
Stefano Sanfilippo
4
A unionnie jest tym, co John Doe również uważałby za klasę . Jednak mogą pełnić funkcje członkowskie. A klasy tak naprawdę nie są obowiązkowe dla funkcji składowych. Używając desygnatora takiego jak valuelub this, coś podobnego enum Size { Huge, Mega, Apocalypse; bool operator<(X rhs) const { return *this < rhs; }(tutaj również pozwalającego ;), może to mieć taki sam sens, jak inne formy funkcji.
Sebastian Mach
85

Chociaż odpowiedź „nie możesz” jest technicznie poprawna, uważam, że możesz być w stanie osiągnąć pożądane zachowanie, korzystając z następującego pomysłu:

Wyobrażam sobie, że chcesz napisać coś takiego:

Fruit f = Fruit::Strawberry;
f.IsYellow();

Miałeś nadzieję, że kod wygląda mniej więcej tak:

enum class Fruit : uint8_t
{
  Apple, 
  Pear,
  Banana,
  Strawberry,

  bool IsYellow() { return this == Banana; }
};

...

Ale oczywiście to nie działa, ponieważ wyliczenia nie mogą mieć metod (a „to” nic nie znaczy w powyższym kontekście)

Jeśli jednak zastosujesz ideę normalnej klasy zawierającej wyliczenie niebędące klasą i pojedynczą zmienną składową, która zawiera wartość tego typu, możesz bardzo zbliżyć się do wymaganej składni / zachowania / bezpieczeństwa typu. to znaczy:

class Fruit
{
public:
  enum Value : uint8_t
  {
    Apple,
    Pear,
    Banana,
    Strawberry
  };

  Fruit() = default;
  constexpr Fruit(Value aFruit) : value(aFruit) { }

#if Enable switch(fruit) use case:
  operator Value() const { return value; }  // Allow switch and comparisons.
                                            // note: Putting constexpr here causes
                                            // clang to stop warning on incomplete
                                            // case handling.
  explicit operator bool() = delete;        // Prevent usage: if(fruit)
#else
  constexpr bool operator==(Fruit a) const { return value == a.value; }
  constexpr bool operator!=(Fruit a) const { return value != a.value; }
#endif

  constexpr bool IsYellow() const { return value == Banana; }

private:
  Value value;
};

Teraz możesz napisać:

Fruit f = Fruit::Strawberry;
f.IsYellow();

Kompilator zapobiegnie takim sytuacjom, jak:

Fruit f = 1;  // Compile time error.

Możesz łatwo dodać takie metody, że:

Fruit f("Apple");

i

f.ToString();

mogą być obsługiwane.

jtlim
źródło
1
Czy nie powinno być również IsYellow (), operator ==,! = Oznaczony jako constexpr?
Jarek C
Otrzymuję komunikat „błąd: brak operatora binarnego przed przełącznikiem tokena”
Pedro77,
18

Koncentrując się na opisie pytania zamiast na tytule, możliwa jest odpowiedź

struct LowLevelMouseEvent {
    enum Enum {
        mouse_event_uninitialized = -2000000000, // generate crash if try to use it uninitialized.
        mouse_event_unknown = 0,
        mouse_event_unimplemented,
        mouse_event_unnecessary,
        mouse_event_move,
        mouse_event_left_down,
        mouse_event_left_up,
        mouse_event_right_down,
        mouse_event_right_up,
        mouse_event_middle_down,
        mouse_event_middle_up,
        mouse_event_wheel
    };
    static const char* ToStr (const type::LowLevelMouseEvent::Enum& event)
    {
        switch (event) {
            case mouse_event_unknown:         return "unknown";
            case mouse_event_unimplemented:   return "unimplemented";
            case mouse_event_unnecessary:     return "unnecessary";
            case mouse_event_move:            return "move";
            case mouse_event_left_down:       return "left down";
            case mouse_event_left_up:         return "left up";
            case mouse_event_right_down:      return "right down";
            case mouse_event_right_up:        return "right up";
            case mouse_event_middle_down:     return "middle down";
            case mouse_event_middle_up:       return "middle up";
            case mouse_event_wheel:           return "wheel";
            default:
                Assert (false);
                break;
        }
        return "";
    }
};
Márkus Attila
źródło
4

Jak wspomniano w drugiej odpowiedzi , nie. Nawet enum classnie jest klasą.


Zwykle potrzeba posiadania metod dla enumwyników wynika z tego, że nie jest to zwykłe (tylko zwiększające się) wyliczenie, ale rodzaj bitowej definicji wartości, które mają być maskowane lub wymagają innych operacji arytmetycznych:

enum class Flags : unsigned char {
    Flag1 = 0x01 , // Bit #0
    Flag2 = 0x02 , // Bit #1
    Flag3 = 0x04 , // Bit #3
    // aso ...
}

// Sets both lower bits
unsigned char flags = (unsigned char)(Flags::Flag1 | Flags::Flag2);

// Set Flag3
flags |= Flags::Flag3;

// Reset Flag2
flags &= ~Flags::Flag2;

Oczywiście można pomyśleć o hermetyzowaniu operacji niezbędnych do ponownego ustawienia / ustawienia pojedynczych / grup bitów, np. Poprzez wartość maski bitowej lub nawet operacje sterowane indeksem bitowym, które byłyby przydatne do manipulowania takim zestawem „flag”.

Plik struct/ class specyfikacja obsługuje tylko lepsze określanie zakresu wartości wyliczenia w celu uzyskania dostępu. Nie więcej nie mniej!

Sposobami na wyjście z tego ograniczenia, których nie można zadeklarować metod dla enum (klas), są użycie std::bitset(klasy opakowania) lub pola bitowegounion .

unions, a takie związki bitfield mogą mieć metody (zobacz tutaj ograniczenia!).

Mam próbkę, jak przekonwertować wartości maski bitowej (jak pokazano powyżej) na odpowiadające im indeksy bitowe, których można użyć std::bitsettutaj: BitIndexConverter.hpp
Znalazłem to bardzo przydatne do zwiększenia czytelności niektórych decyzji `` flag '' algorytmy.

πάντα ῥεῖ
źródło
36
Jest więcej przypadków użycia, które uzasadniają metody na enum classe, np. ToString () i fromString (). Każdy (nawet nie tak) współczesny główny język ma to (np. C #, Java, Swift), ale nie C ++.
Mike Lischke
1
Miejmy nadzieję, że następnym razem będziemy
sdgfsdh
4

Istnieje całkiem kompatybilna zdolność (§) do refaktoryzacji wyliczenia na klasę bez konieczności przepisywania kodu, co oznacza, że efektywnie możesz robić to, o co prosiłeś, bez zbytniej edycji.

(§) jak ElementW wskazuje w komentarzu, kod zależny od type_traits nie będzie działał, więc np. Nie można użyć auto, itp. Może być jakiś sposób na takie rzeczy, ale w końcu jest to konwersja wyliczenia na klasę, i zawsze błędem jest obalanie C ++

enum structi enum classspecyfikacje są o określenie zakresu, więc nie brać w tym udziału.

Twoje oryginalne wyliczenie to np. „Zwierzak” (to jest tylko przykład!).

enum pet { 
    fish, cat, dog, bird, rabbit, other 
};

(1) Zmieniasz to na np. PetEnum (aby ukryć go przed istniejącym kodem).

enum petEnum { 
    fish, cat, dog, bird, rabbit, other 
};

(2) Dodajesz pod nią nową deklarację klasy (nazwaną oryginalnym wyliczeniem)

class pet {
    private:
        petEnum value;
        pet() {}

    public:
        pet(const petEnum& v) : value{v} {} //not explicit here.
        operator petEnum() const { return value; }
        pet& operator=(petEnum v) { value = v; return *this;}
        bool operator==(const petEnum v) const { return value == v; }
        bool operator!=(const petEnum v) const { return value != v; }
 //     operator std::string() const;

};

(3) Możesz teraz dodać dowolne metody klas do swojej klasy zwierząt domowych. na przykład. operator łańcuchowy

    pet::operator std::string() const {
        switch (value) {
            case fish: return "fish";
            case cat:  return "cat";
            case dog:  return "dog";
            case bird: return "bird";
            case rabbit: return "rabbit";
            case other: return "Wow. How exotic of you!";
        }
    }

Teraz możesz użyć np. Std :: cout ...

int main() {
    pet myPet = rabbit;
    if(myPet != fish) {
        cout << "No splashing! ";
    }
    std::cout << "I have a " << std::string(myPet) << std::endl;
    return 0;
}
Konchog
źródło
1
To nie w pełni kompatybilny: jeśli używasz wartości enum z jakiegokolwiek rodzaju odliczenia, gdzie oczekuje się, aby uzyskać petTypeName / instancji, czy to szablony, autolub decltype, psuje to, co dostajesz petEnumw zamian.
ElementW,
0

Może nie spełnia wszystkich twoich potrzeb, ale z operatorami niebędącymi członkami nadal możesz się dobrze bawić. Na przykład:

#include <iostream>

enum class security_level
{
    none, low, medium, high
};

static bool operator!(security_level s) { return s == security_level::none; }

static security_level& operator++(security_level& s)
{
    switch(s)
    {
        case security_level::none: s = security_level::low; break;
        case security_level::low: s = security_level::medium; break;
        case security_level::medium: s = security_level::high; break;
        case security_level::high: break;
    }
    return s;
}

static std::ostream & operator<<(std::ostream &o, security_level s)
{
    switch(s)
    {
        case security_level::none: return o << "none";
        case security_level::low: return o << "low";
        case security_level::medium: return o << "medium";
        case security_level::high: return o << "high";
    }
}

Umożliwia to kod taki jak

security_level l = security_level::none;   
if(!!l) { std::cout << "has a security level: " << l << std::endl; } // not reached
++++l;
if(!!l) { std::cout << "has a security level: " << l << std::endl; } // reached: "medium"
Johannes
źródło