Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Solution to mimic enum inheritance in cpp

I know that enum inheritance is not possible in c++, but I am looking for specific data structure that simply fit into my case. Suppose I have these two enums:

    enum Fruit { apple, orange};
    enum Drink { water, milk};

I want a parent for these two that I can use as parameter in this abstract method

   void LetsEat(Eatable eatable){}

They are going to be used as simple switches, and basically I want to keep my code clean and type safe. I wonder if I will be forced to use inherited classes that needs to be initialized. It is too much for this simple problem.

like image 432
Aryan Firouzian Avatar asked Sep 04 '21 01:09

Aryan Firouzian


People also ask

Can enum inherit in C++?

Not possible. There is no inheritance with enums. You can instead use classes with named const ints.

Can enum class be inherited?

Java Enum and Interface As we have learned, we cannot inherit enum classes in Java. However, enum classes can implement interfaces.

Can enum values be changed C++?

Solution 1. You cannot modify at runtime a C++ enum.

How do you declare an enum in C++?

An enumeration is a user-defined data type that consists of integral constants. To define an enumeration, keyword enum is used. enum season { spring, summer, autumn, winter }; Here, the name of the enumeration is season .


3 Answers

Speaking very generally, enums are just dressed up ints.

enum Fruit { apple, orange};

If you look at the compiled code, you will discover that an apple will be represented by the value 0, and an orange will be represented by the value 1.

enum Drink { water, milk};

Same thing here will happen here. water will be represented by value 0, and milk will be represented by value 1. You can begin to see the obvious problem here.

One, a slightly primitive, solution is equivalent to letting a bull loose in the china shop:

enum Drink { water=2, milk=3};

Now you could cook something up where you're passing in a int value and figure out what exactly was passed in, by its value.

But this will likely require plenty of ugly casts, everywhere. The resulting code, if posted to Stackoverflow, will likely to attract downvotes.

The downvotes will be because there are cleaner solutions that are available in modern, post C++17 world. For starters, you can switch to enum classes.

enum class Fruit { apple, orange};
enum class Drink { water, milk};

This gains additional type-safety. It's not as easy, any more, to assign a Fruit to a Drink. Your C++ compiler will bark, very loudly, in many situations where it would raise a warning. Your C++ compiler will help you find even more bugs, in your code. It is true that this will require a little bit more typing. You will always have to specify enumerated values everywhere with full qualification, i.e. Fruit::apple and Drink::water, when in your existing code a mere apple and water will suffice. But a few extra typed characters is a small price to pay for more type-safe code, and for being able to simply declare:

typedef std::variant<Fruit, Drink> Eatable;

and simply do what you always wanted:

void LetsEat(Eatable eatable){}

and everything will work exactly how you wanted it. LetsEat will accept either a Fruit or a Drink as its parameter. It will have to do a little bit more work, to figure out what's in the std::variant, but nobody ever claimed that C++ is easy.

std::variant is one of the more complex templates in the C++ library, and it's not possible to explain how to use it, fully, in a short paragraph or two on Stackoverflow. But this is what's possible, and I'll refer you to your C++ textbook for a complete description of how to use this template.

like image 86
Sam Varshavchik Avatar answered Nov 18 '22 18:11

Sam Varshavchik


This sounds like an excellent use case for std::variant.

#include <variant>
#include <iostream>

// Define our enums
enum Fruit { Apple, Orange };
enum Drink { Water, Milk };

// An Eatable is either a Fruit or a Drink
using Eatable = std::variant<Fruit, Drink>;

void letsEat(Eatable eatable) {
  // We can use the index() method to figure out which one we have
  switch (eatable.index()) {
  case 0:
    std::cout << "It's a Fruit!" << std::endl;
    break;
  case 1:
    std::cout << "It's a Drink!" << std::endl;
    break;
  }
}

int main() {
  letsEat(Apple);
  letsEat(Water);
}

Note that std::variant<Fruit, Drink> is not, strictly speaking, a supertype of Fruit or Drink. Instead, it's a new type altogether but we get implicit conversions from Fruit and Drink to std::variant<Fruit, Drink> via its constructors.

If you're not using C++17, you can use boost::variant from the Boost C++ libraries.

like image 44
Silvio Mayolo Avatar answered Nov 18 '22 18:11

Silvio Mayolo


You can use std::variant<T, ...> if you use C++17 or above:

#include <iostream>
#include <variant>
#include <type_traits>

enum Fruit { apple, orange };
enum Drink { water, milk };

using Eatable = std::variant<Fruit, Drink>;

void LetsEat(Eatable const eatable) {
    std::visit([] (auto&& v) {
        using T = std::decay_t<decltype(v)>;
        if constexpr (std::is_same_v<T, Fruit>) {
            // Now use it like you would use a normal 'Fruit' variable ...
        }
        if constexpr (std::is_same_v<T, Drink>) {
            // Now use it like you would use a normal 'Drink' variable ...
        }
    }, eatable);
}

int main() {
    LetsEat(apple);
}

Alternatively, you could just create a class that is implicitly convertible to either enum type:

class Eatable {
    union {
        Fruit f;
        Drink d;
    } u_;
    bool has_fruit_;
public:
    Eatable(Fruit f) : has_fruit_(true) {
        u_.f = f;
    };
    Eatable(Drink d) : has_fruit_(false) {
        u_.d = d;
    };
    operator Fruit() const {
        return u_.f;
    }
    operator Drink() const {
        return u_.d;
    }
    bool has_fruit() const {
        return has_fruit_;
    }
};

Then you can use it like this:

void LetsEat(Eatable const eatable) {
    if (eatable.has_fruit()) {
        Fruit const f = eatable;
        switch (f) {
            case apple:
                std::cout << "Fruit: apple" << std::endl;
                break;
            case orange:
                std::cout << "Fruit: orange" << std::endl;
                break;
            default: break;
        }
    } else {
        Drink const d = eatable;
        switch (d) {
            case water:
                std::cout << "Drink: water" << std::endl;
                break;
            case milk:
                std::cout << "Drink: milk" << std::endl;
                break;
            default: break;
        }
    }
}
like image 36
Ruks Avatar answered Nov 18 '22 18:11

Ruks