Home > database >  C Inheritance: arguments of derived class type in virtual function with base class types
C Inheritance: arguments of derived class type in virtual function with base class types

Time:09-18

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:

  1. Creating an eat_food(Food f) implementation in Cow with a try/catch to check if f can be safely casted to Vegetables, and then calling eat_food(Vegetables v). PROBLEM: if you have 50 virtual functions, this forces you to write 50 additional function implementations in Cow.
  2. Turn the Animal into a Template class Animal<T>, and instantiate it with each of the derived classes of Food to define the animals (e.g., class Cow: public Animal<Vegetables>). PROBLEM: you can no longer define an Animal* 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
  • Related