I'm having a rough time with a particular C inheritance problem. Say we have two abstract classes, one using the other as argument type for one of the pure virtual functions:
class Food {
public:
int calories=0;
virtual void set_calories(int cal)=0;
}
class Animal {
public:
int eaten_calories=0;
virtual void eat_food(Food &f)=0;
}
Now, we create a derived class for each, and we instantiate a virtual function with arguments of type the derived class:
class Vegetables: public Food{
public:
void set_calories(int cal){calories=cal;}
}
class Cow: public Animal{
public:
void eat_food(Vegetables &v){this->eaten_calories = v.calories;}
}
The problem with this is that the function eat_food
requires a signature with the abstract class Food
, or else a Cow()
object creation won't compile, complaining that Cow
is an abstract class because no suitable implementation of eat_food(Food f)
was found.
Update: An additional constraint I seek for the implementation is that a second class Meat: public Food
should not be usable with Cow::eat_food(f)
. In short, just setting Cow::eat_food(Food f)
and casting to Vegetables
wouldn't cut it.
What is the best way to overcome this error?
So far I have found two options:
- Creating an
eat_food(Food f)
implementation inCow
with atry/catch
to check iff
can be safely casted toVegetables
, and then callingeat_food(Vegetables v)
. PROBLEM: if you have 50 virtual functions, this forces you to write 50 additional function implementations inCow
. - Turn the
Animal
into a Template classAnimal<T>
, and instantiate it with each of the derived classes ofFood
to define the animals (e.g.,class Cow: public Animal<Vegetables>
). PROBLEM: you can no longer define anAnimal*
pointer to hold an undefined animal with not known type.
Is there any viable/stylish alternative to these two? Maybe a software pattern of some kind?
CodePudding user response:
When you defined the virtual function Animal::eat_food()
accepting a Food&
parameter, you declared that for any Animal
, you can provide any Food
to eat_food()
. Now you want to break that promise. This brings into question your design. Either it is legitimate to call ptr->eat_food(food)
where ptr
is an Animal*
and food
is a Meat
, or eat_food()
should (probably) not be defined in the Animal
class. If you cannot substitute one Food
for another, use of Food&
is likely a mistake. If you cannot substitute one Animal
for another, defining at the Animal
level is likely a mistake.
Perhaps a small change in nomenclature would help this make more sense. Consider renaming eat_food
to give_food
, or perhaps feed
. Now you have a concept that is applicable to all animals. You can feed any food to any animal, but whether or not the animal eats it is a different story. Maybe you should make your virtual function feed()
so that it applies equally well to all animals. If you have an Animal*
and a Food&
, you can feed the animal, but it's the animal that decides if it eats. If you were to instead insist that you must know the correct type of Food
before feeding the Animal
, then you should have a Cow*
instead of an Animal*
.
Note: If you happen to be in a case where you never try to feed an Animal*
, then you could remove the virtual function from Animal
, in which case your question becomes moot.
This might look something like the following.
class Animal {
int eaten_calories=0;
protected:
void eat_food(Food &f) { eaten_calories = f.calories; } // Not virtual
public:
virtual void feed(Food &f)=0;
};
class Cow: public Animal{
public:
void feed(Food &f) override {
// Cows only eat Vegetables.
if ( dynamic_cast<Vegetables*>(&f) ) // if `f` is a Vegetables
eat_food(f);
else
stampede(); // Or whatever you think is amusing (or appropriate).
}
};
Note that I have kept your eat_food()
implementation, but moved it to a non-virtual function in Animal
. This is based on an assumption, so it might be inappropriate. However, I am willing to assume that no matter what type of animal, and no matter what type of food, if the animal actually eats the food, then the eaten calories should increase by the food's calories.
In addition, a rule of thumb says that this might be the correct level of abstraction -- the two bits of data being used, calories
and eaten_calories
, belong directly to the two classes being used, Animal
and Food
. This suggests (just a rule of thumb) that your logic and data are at a consistent level of abstraction.
Oh, I also specified protected
access for eat_food()
. This way it is the object's decision whether or not to eat. No one will be able to force an animal to eat; they would only be able to offer it food. This demonstrates another principle of polymorphic design: when derived classes differ, only the objects of those classes should need to be aware of those differences. Code that sees only objects of a common base should not need to test for these differences in advance of using those objects.
CodePudding user response:
If you pass around a polymorphic type (like Vegetables
) as a base type by value (like Food f
), you will slice the object, which prevents overriden methods from being called.
You need to pass such types by pointer or by reference instead, eg:
class Food {
public:
virtual int get_calories() const = 0;
};
class Animal {
public:
int eaten_calories = 0;
virtual void eat_food(Food& f) = 0;
};
class Vegetables: public Food {
public:
int get_calories() const { return ...; }
};
class Cow: public Animal{
public:
void eat_food(Food& f){ this->eaten_calories = f.get_calories(); }
};
Vegetables veggies;
Cow cow;
cow.eat_food(veggies);
UPDATE:
You can't change the signature of a virtual method in derived classes (except when using covariant return types). Since eat_food()
is exposed in Animal
and takes a Food&
, if you want Cow::eat_food()
to accept only a Vegetables
object and not a Meat
object, then it needs to check at runtime if the input Food&
refers to a Vegetables
object and if not then throw an exception. dynamic_cast
does exactly that for you when casting a reference, eg:
class Cow: public Animal{
public:
void eat_food(Food& f){ this->eaten_calories = dynamic_cast<Vegetables&>(f).calories; }
};
Vegetables veggies;
Meat meat;
Cow cow;
cow.eat_food(veggies); // OK
cow.eat_food(meat); // throws std::bad_cast