I know that enum inheritance is not possible in c , but I am looking for specific data structure that simply solve 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.
CodePudding user response:
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.
CodePudding user response:
Speaking very generally, enum
s are just dressed up int
s.
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.
CodePudding user response:
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;
}
}
}