C++ 继承:具有基本 class 类型的虚函数中的派生 class 类型的参数

C++ Inheritance: arguments of derived class type in virtual function with base class types

我遇到了一个特定的 C++ 继承问题。假设我们有两个抽象 classes,一个使用另一个作为纯虚函数之一的参数类型:

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;
}

现在,我们为每个创建一个派生的 class,并且我们用派生的 class:[=35 类型的参数实例化一个虚函数=]

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;}
}

这个问题是函数 eat_food 需要带有抽象 class Food 的签名,否则 Cow() 对象创建将无法编译,抱怨 Cow 是一个抽象 class 因为没有找到 eat_food(Food f) 的合适实现。

Update:我寻求实现的另一个限制是第二个 class Meat: public Food 不应该与 Cow::eat_food(f)。简而言之,仅设置 Cow::eat_food(Food f) 并转换为 Vegetables 不会削减它。

克服此错误的最佳方法是什么?

到目前为止我找到了两个选项:

  1. 使用 try/catchCow 中创建一个 eat_food(Food f) 实现来检查 f 是否可以安全地转换为 Vegetables,然后调用 eat_food(Vegetables v)问题:如果你有 50 个虚函数,这会迫使你在 Cow.
  2. 中编写 50 个额外的函数实现
  3. Animal 转换为模板 class Animal<T>,并使用 Food 的每个派生 classes 对其进行实例化以定义动物(例如,class Cow: public Animal<Vegetables>)。 问题:你不能再定义一个Animal*指针来保存未知类型的未定义动物。

这两个有什么viable/stylish替代品吗?也许是某种软件模式?

如果将多态类型(如 Vegetables)作为基类型按值(如Food f)传递,你将slice the object,它可以防止重写的方法被调用。

您需要通过指针通过引用传递此类类型,例如:

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);

更新:

您不能更改派生 类 中虚方法的签名(使用协变 return 类型时除外)。由于 eat_food()Animal 中暴露并接受 Food&,如果您希望 Cow::eat_food() 仅接受 Vegetables 对象而不接受 Meat 对象,然后它需要在运行时检查输入 Food& 是否引用 Vegetables 对象,如果不是则抛出异常。 dynamic_cast 在引用时为你做的正是这样,例如:

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

当您定义虚函数 Animal::eat_food() 接受一个 Food& 参数时,您声明对于任何 Animal,您可以提供任何 Foodeat_food().现在你想打破这个承诺。这使您的设计受到质疑。调用 ptr->eat_food(food) 是合法的,其中 ptrAnimal* 并且 foodMeat,或者 eat_food() 应该(可能)不是在 Animal class 中定义。如果您不能用一个 Food 代替另一个,则使用 Food& 很可能是错误的。如果您不能用一个 Animal 替换另一个,则在 Animal 级别定义可能是一个错误。

也许在命名法上做一个小的改变会让这更有意义。考虑将 eat_food 重命名为 give_food,或者 feed。现在你有了一个适用于所有动物的概念。您可以将任何食物喂给任何动物,但动物是否吃它是另一回事。也许你应该让你的虚函数 feed() 让它同样适用于所有动物。如果你有一个 Animal* 和一个 Food&,你可以喂动物,但它是否吃东西是由动物决定的。如果你坚持要在输入 Animal 之前必须知道 Food 的正确类型,那么你应该使用 Cow* 而不是 Animal*.

注意:如果您碰巧处于从未尝试喂养 Animal* 的情况,那么您可以从 Animal 中删除虚函数],在这种情况下,您的问题就没有实际意义了。

这可能类似于以下内容。

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).
    }
};

请注意,我保留了您的 eat_food() 实现,但将其移至 Animal 中的非虚函数。这是基于一个假设,因此可能不合适。不过,我愿意假设,不管是什么动物,不管是什么食物,如果动物真的吃掉了食物,那么吃进去的热量应该会增加食物的热量。

此外,根据经验,这可能是正确的抽象级别——正在使用的两位数据 calorieseaten_calories 直接属于两个 class正在使用,AnimalFood。这表明(只是经验法则)您的逻辑和数据处于一致的抽象级别。

哦,我还为 eat_food() 指定了 protected 访问权限。这样是对象自己决定吃不吃。没有人能够强迫动物进食;他们只能给它食物。这证明了多态设计的另一个原则:当派生的 classes 不同时,只有那些 classes 的对象需要知道这些差异。仅查看公共基础对象的代码在使用这些对象之前不需要测试这些差异。