【问题标题】:How to implement a virtual function with different signatures?如何实现具有不同签名的虚函数?
【发布时间】:2021-10-16 18:57:51
【问题描述】:

我想要做的是暴露一个成员函数的名字告诉其他开发者每个派生类都必须实现接口中暴露的同名的特定函数,但是实现的函数可以使用不同的签名。

要使用gtest测试接口,我必须保留示例中的虚函数:

class Fruit {};
class Meat {};
class Finger {};

class Animal {
 public:
  virtual void eat(/* something */) = 0;
};

class Monkey : public Animal {
 public:
  void eat(Fruit fruit, Finger finger) override {
    std::cout << "I eat fruits and suck my fingers" << std::endl;
  }
};

class Dog : public Animal {
 public:
  void eat(Meat meat) override {
    std::cout << "I eat meat" << std::endl;
  }
};

int main() {
  Meat meat;
  Fruit fruit;
  Finger finger;
  Monkey monkey;
  Dog dog;
  monkey.eat(fruit, finger);
  dog.eat(meat);
}

看完后:

我知道有两种可能的解决方案,一种是使用调度程序,另一种是使用 CRTP。

调度员: 此解决方案会破坏类型检查。

class EatDispatcherInterface {
 public:
  virtual void eat(Monkey* monkey) = 0;
  virtual void eat(Dog* dog) = 0;
};

class EatDispatcher : public EatDispatcherInterface {
 public:
  EatDispatcher(Fruit fruit) : food(fruit) {}
  EatDispatcher(Meat meat) : food(meat) {}
  void eat(Monkey* monkey) override {
    std::cout << "I eat fruits" << std::endl;
  }
  void eat(Dog* dog) override {
    std::cout << "I eat meat" << std::endl;
  }
 private:
  union {
    Fruit fruit;
    Meat meat;
  } food;
};

class Fruit {};
class Meat {};

class Animal {
 public:
  virtual void accept(const EatDispatcher& eat_dispatcher) = 0;
};

class Monkey : public Animal {
 public:
  void eat(const EatDispatcher& eat_dispatcher) override {
    eat_dispatcher.eat(this);
  }
};

class Dog : public Animal {
 public:
  void eat(const EatDispatcher& eat_dispatcher) override {
    eat_dispatcher.eat(this);
  }
};

int main() {
  EatDispatcher fruit_eat_dispatcher(Fruit());
  EatDispatcher meat_eat_dispatcher(Meat());
  Monkey monkey;
  Dog dog;
  monkey.accept(fruit_eat_dispatcher);
  dog.accept(meat_eat_dispatcher);
}

CRTP: 这个解决方案在接口中暴露了派生类的细节。

class Fruit {};
class Meat {};

template <typename Food>
class Animal {
 public:
  virtual void eat(Food food) = 0;
 };

class Monkey : public Animal<Fruit> {
 public:
  void eat(Fruit fruit) {
    std::cout << "I eat fruits" << std::endl;
  }
};

class Dog : public Animal<Meat> {
 public:
  void eat(Meat meat) {
    std::cout << "I eat meat" << std::endl;
  }
};

int main() {
  Monkey monkey;
  Dog dog;
  monkey.eat(Fruit());
  dog.eat(Meat());
}

正确的模式是什么?有没有没有这些缺点的其他解决方案?

版权所有 2021 Google LLC。

SPDX 许可证标识符:Apache-2.0

【问题讨论】:

  • override 关键字只是告诉编译器签名应该与基类中的虚函数 100% 匹配的一种方式,将其删除,它将起作用 just fine
  • @Ruks 它不会再覆盖基类,它将是一个具有不同签名的新函数
  • 当您的基类是模板类时,它不再可用作基类。该模板使其对每种食物都独一无二。
  • 我没有看到多态性的任何用途,因此您的示例中需要虚函数?
  • "我想要做的是暴露一个成员函数的名字告诉其他开发者每个派生类都必须实现接口中暴露的同名的特定函数,但是实现的函数可以使用不同的签名。”这只是文档吗?如果您不知道如何调用 eat 成员函数,那么知道它的存在有什么帮助?

标签: c++ design-patterns


【解决方案1】:

这里的问题是,“具有不同签名的虚函数”是什么?

严格来说,你想要的是不可能的:在 C++ 中,如果一个函数覆盖了祖先类的成员函数,那么这两个函数根据定义具有相同的签名。所以你想要的是一些代码,其行为就好像它实现了一个具有多个签名的虚函数。

好的,那是什么意思?好吧,这取决于您要将代码用于什么目的。例如,虚函数的一大用途是通过指向基类的指针进行多态调用,而不必知道指针指向的对象的具体类。在您的问题代码中,Dispatcher 解决方案将允许您执行此操作。如果这是您遇到的问题,它会解决那个问题。

CRTP 版本不能解决这个问题。 Monkey 和 dog 不共享一个基类——一个继承自 Animal&lt;Fruit&gt;,另一个继承自 Animal&lt;Meat&gt;,因此您不能对动物进行多态 eat 调用,因为您不能拥有指向抽象动物的指针。但是,CRTP 版本允许您通过指向食肉动物的不透明指针或指向草食动物的不透明指针进行多态调用,如果这是您需要做的事情,而 Dispatcher 版本则不会。

我要说的是,由于您要求的东西在语言中的语法和语义上是不可能的,因此问题的格式不正确,因为答案取决于您要完成的目标是什么提出的问题。

【讨论】:

    【解决方案2】:

    我想要做的是暴露一个成员函数的名字告诉其他开发者每个派生类都必须实现接口中暴露的同名的特定函数,但是实现的函数可以使用不同的签名。

    没有办法做到这一点。这没有道理。就算你能做到,那也完全没用。

    出于某种原因,虚拟函数必须在整个层次结构中具有相同的签名。您可以在不知道对象的确切类型的情况下调用该函数。签名是合同。合同在整个层次结构中强制执行,以便用户可以依赖它。如果用户有一个指向Base 的指针,他们可以调用它上面的任何虚方法,而不管对象的动态类型。

    Animal 的开发者没有理由强迫Monkey 的开发者使用他们喜欢的任何签名来实现eat。此方法不能从对Animal 的引用指针中调用,因此Animal 开发人员无需为非Animal 的类指定它的存在。

    但我希望 Monkey::eat 可以使用 Animal 指针或引用来调用!

    如果你知道你有一个Monkey 和一个Fruit,只需为你的变量使用正确的类型。不是Animal* theAnimal,而是Monkey* theAnimal。无需从Animal 调用eat

    如果你知道你有一些动物和一些与该动物相容的食物,但不知道具体是什么,你最好用你的类型来表达这些知识变量。像AnimalAndFood 这样的抽象基类可能很有用,派生类如MonkeyAndFruitDogAndMeat。没有MonkeyAndMeat,所以不存在食物不匹配的风险。 template &lt;class Food&gt; class Animal 属于同一大类。

    如果您知道自己有一些动物和一些食物,但不确定它们的兼容性,则需要进行运行时检查。

    有点我不喜欢这些解决方案!

    这就是我们拥有的所有工具。

    【讨论】:

      【解决方案3】:

      你可以做一些事情,虽然我不知道我是否关心这些。

      我既不喜欢调度程序,也不喜欢暴露派生类的细节。但是……

      如果可以吃的东西的列表很小且可控,则可以在基类中为每一个都实现一个eat()方法。

      如果列表很长或可能是动态的,那么您可以决定它们必须全部派生自 Food,并且您还可以在 Food 中有一个字段,即食物类型。然后eat 方法可以采用Food,并且您的子类检查该类型是否是动物识别为食物的东西。这可能是我会做的。

      或者您可以使用模板化方法做一些聪明的事情。我不太确定是什么,因为你的例子是人为的。

      【讨论】:

      • 如果我包含Food,我将不得不使用我不想使用的dynamic_cast
      • 也许不是,但这比您可能添加的批量要好。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-11-11
      • 2011-05-11
      • 1970-01-01
      • 1970-01-01
      • 2018-07-16
      相关资源
      最近更新 更多