【问题标题】:C++ Inheritance: arguments of derived class type in virtual function with base class typesC ++继承:具有基类类型的虚函数中派生类类型的参数
【发布时间】:2021-11-02 20:02:06
【问题描述】:

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

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 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需要一个抽象类Food的签名,否则Cow()对象创建将无法编译,抱怨Cow是一个抽象类,因为没有合适的找到了eat_food(Food f) 的实现。

更新:我寻求实现的另一个限制是第二个class Meat: public Food 不应该与Cow::eat_food(f) 一起使用。简而言之,仅设置 Cow::eat_food(Food f) 并强制转换为 Vegetables 不会成功。

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

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

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

这两者有什么可行/时尚的替代品吗?也许是某种软件模式?

【问题讨论】:

    标签: c++ inheritance design-patterns abstract-class virtual-functions


    【解决方案1】:

    当您定义接受Food& 参数的虚函数Animal::eat_food() 时,您声明对于任何Animal,您可以将任何Food 提供给eat_food()。现在你想打破这个承诺。这会质疑您的设计。调用ptr->eat_food(food) 是合法的,其中ptrAnimal*foodMeat,或者eat_food() 不应该(可能)在Animal 类中定义。如果您不能将一个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,直接属于所使用的两个类,AnimalFood。这表明(只是一个经验法则)您的逻辑和数据处于一致的抽象级别。

    哦,我还为eat_food() 指定了protected 访问权限。这样,是否吃东西由对象决定。没有人可以强迫动物吃东西;他们只能提供食物。这证明了多态设计的另一个原则:当派生类不同时,只有这些类的对象需要知道这些差异。只看到公共基础对象的代码不需要在使用这些对象之前测试这些差异。

    【讨论】:

    • 你肯定把动物和食物的例子拿得太远了!感谢您的建议。
    【解决方案2】:

    如果您将多态类型(如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);
    

    更新:

    您不能更改派生类中虚方法的签名(使用协变返回类型时除外)。由于eat_food()Animal 中公开并采用Food&amp;,如果您希望Cow::eat_food() 只接受Vegetables 对象而不接受Meat 对象,那么它需要在运行时检查输入@ 987654332@ 指的是 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
    

    【讨论】:

    • 不错且干净的答案...我刚刚意识到我错过了我的问题的一个限制!我马上更新。
    • 完成:简而言之,我应该怎么做才能让Cow 只吃Vegetables,而不吃class Meat: public Food
    • 我还更新了问题以通过引用传递参数(就像我的实际代码中的情况一样)。
    • @ibarrond 虚拟方法中的协方差仅适用于返回值,不适用于参数。我认为您唯一的选择是让Cow::eat_food() 检查输入Food 引用是否引用Vegetables 对象,如果不是则throw 异常。 dynamic_cast 在投射参考时会为您做这些,例如:void eat_food(Food &amp;f){ this-&gt;eaten_calories += dynamic_cast&lt;Vegetables&amp;&gt;(f).calories; }
    • 不错! dynamic_cast 很优雅,它提供了一种避免重复函数名称的简单方法。我会用它修改你的答案
    猜你喜欢
    • 2016-10-08
    • 2015-10-24
    • 1970-01-01
    • 1970-01-01
    • 2015-03-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多