使用纯多态性和继承从基类调用派生类上的函数而不进行强制转换?

Posted

技术标签:

【中文标题】使用纯多态性和继承从基类调用派生类上的函数而不进行强制转换?【英文标题】:Using purely polymorphism and inheritence to call functions on a derived class from base class without casting? 【发布时间】:2020-03-04 12:38:14 【问题描述】:

我的问题是: 我有一个纸牌游戏,它由所有类型的纸牌的奴才、法术和设备组成,我知道方法的虚拟定义。我需要在Minion 上使用GetHealth()RemoveHealth()AddHealth() 函数,但我不想在SpellEquipment 上使用这些函数。我想做的是:

class CCard

//Member variables and functions
;

class CMinion : public CCard

    //Member variables and functions
    int GetHealth() const return mHealth;
;

std::unique_ptr<CCard> minion = std::make_unique<CMinion>(params);
minion->GetHealth(); //I realize this isn’t possible the way it is.

我目前有一个单独的仆从列表,其中包含与卡片相关联的 ID,当卡片在我手中时,我将卡片放在桌子上而不是卡片上。我的讲师想要纯多态性和继承,所以这是一个笨拙的解决方案。 我还讨论了将GetHealth() 和其他必需的方法放在 Card 类中,并使不需要它的类返回 0。他说“这是一个更好的解决方案,但我应该再次使用继承和多态”。

他发现我的架构是错误的,我应该重组它。

我应该有: 卡片向量是MinionSpellEquipment 的实例。桌子、手牌和套牌应该都是卡片的向量,但我目前的解决方案是套牌和手牌是卡片的向量,而我的桌子是 Minions 的向量。

谁能提出更好的结构或有效的解决方案?

【问题讨论】:

使GetHealth成为CCard的纯虚成员函数 @acraig5075: GetHealthSpell/Equipment 没有意义... 注意:手动内存管理无论如何都不是“更简单”,实际代码就可以了。 没有对一种以上类型有意义的操作,但必须对每种类型进行不同的实现 => 不考虑多态性,尽管您的讲师希望如此。 你怎么知道你攻击的是Minion而不是SpellEquipment?你需要double dispatch(或者只是Visitor pattern)吗? 【参考方案1】:

正如 Jarod42 在对原始问题的评论中提到的,您可以使用 visitor pattern。

这使您无需任何强制转换即可获得Card 对象的实际类型。也称为双重调度。

然后,您可以将特定类型卡的特定功能放入该卡的类型中。 IE。无需将默认的虚拟GetHealth () 函数放在引发异常的Card 中。只需在 Minions 卡类型中声明 GetHealth() 函数,Spell 不会有 GetHealth()

这是一个小例子:

// Forward declare visitors so Accept function can be delared in Card.
struct CardVisitor;
struct ConstCardVisitor;

struct Card 
  virtual ~Card () = default;

  virtual void Activate () = 0;

  virtual void Accept (CardVisitor & obj) = 0;
  virtual void Accept (ConstCardVisitor & obj) const = 0;
;

// Forward declare derived Card types, so Visit function can be declared in visitors.
struct Minion;
struct Spell;

struct CardVisitor 
  virtual void Visit (Minion & obj) = 0;
  virtual void Visit (Spell & obj) = 0;
;

struct CardVisitor 
  virtual void Visit (Minion const & obj) = 0;
  virtual void Visit (Spell const & obj) = 0;
;

struct Minion : Card 
  int GetHealth () const;

  virtual void Activate () override;

  // Copy-paste exactly this code in every "final" type deriving from Card.
  virtual void Accept (CardVisitor & obj) override  obj.Visit(*this); 
  virtual void Accept (ConstCardVisitor & obj) const override  obj.Visit(*this); 


struct Spell : Card 
  int GetSmokeColor () const;
  bool DoesInstaKill () const;

  virtual void Activate () override;

  // Copy-paste exactly this code in every "final" type deriving from Card.
  virtual void Accept (CardVisitor & obj) override  obj.Visit(*this); 
  virtual void Accept (ConstCardVisitor & obj) const override  obj.Visit(*this); 

然后创建一个访问者来做任何需要的事情。例如。让我们打印一些关于卡片的信息:

#include <iostream>
#include <memory>

struct PrintVisitor : ConstCardVisitor 
  virtual void Visit (Minion const & obj) override 
    std::cout << "health: " << obj.GetHealth();
  

  virtual void Visit (Spell const & obj) override 
    std::cout << "smoke color: " << obj.GetSmokeColor()
      << ", does insta kill: " << obj.DoesInstaKill();
  


int main () 
  // Base class pointers, i.e. to Card.
  std::unique_ptr<Card> cardA = std::make_unique<Minion>();
  std::unique_ptr<Card> cardB = std::make_unique<Spell>();

  PrintVisitor printVisitor;
  cardA->accept(printVisitor);
  cardB->accept(printVisitor);
;

PS:Card 的析构函数应该是虚拟的。它不在您的示例代码中。在这种情况下,当您在 Card 指针上调用 delete 时(或者当 std::unique_ptr 为您执行此操作时),您的派生对象将不会被正确删除。

编辑:添加了Spell 类以更清楚地利用访问者模式。

【讨论】:

这会起作用,但是对于 GetHealth()、RemoveHealth() 和 AddHealth() 这三种方法 - 我需要将其中的 3 个“Accepts”添加到每个派生类的最小值,这很好。但是我的 Spell 课程也会有这个,即使我可以在拼写中将其留空(因为拼写不需要它),如果我将 GetHealth()、RemoveHealth() 和 AddHealth() 放入我的卡中,这不会以完全相同的方式工作类作为虚拟方法并使它们在拼写中也为空?除非我没有抓住重点,否则对于相同的结果似乎少了很多努力? 我将使用Spell 类扩展示例。关键是您只添加GetHealth() 那些有意义的类。您确实需要为每个派生类实现Accept()。关键是,在每个实现中,Accept() 调用 Visit() 时使用实际对象的类型作为参数,即 Visit() 的参数类型永远不会是 Card &amp; 感谢您的澄清,我知道这对我有什么好处,但是除了他们每个人都有的生命值和激活方法之外,Minion, Spell 和 Equipment 类之间没有区别。也就是说,如果我的每个类都有我想调用但不想放入基类的东西,我肯定会参考这个解决方案。一个非常有用的方法。谢谢你:)【参考方案2】:

我会这样做:

class Card

public:
    virtual ~Card() = default;

    template <typename T>
    T* As()  return dynamic_cast<T*>(this); 
;

class Minion : public Card

public:
    int GetHealth() const return mHealth;

private:
    int mHealth = 100;
;

void test()

    std::unique_ptr<Card> card = std::make_unique<Minion>();
    if (auto* minion = card->As<Minion>())
        minion->GetHealth();

if (auto* minion = ... 部分是检查类型是否为 Minion 并获取指向它的指针的紧凑方法,以便您可以调用基类中不存在的方法。

【讨论】:

它说标题中没有演员表,这包括所有演员表,无论是dynamic_cast,static_cast还是任何其他演员表。讲师说他不想那样。 @XenoXetric 您可能误解了作业中的某些内容。不强制转换,不可能通过基类引用或指针调用只存在于子类中的成员函数。 @NikosC。使用访问者模式完全有可能(请参阅 Jarod42 对原始问题的评论)。不需要演员表。【参考方案3】:

您要么需要将GetHealth 放入Card,要么稍后使用强制转换。别无退路。由于演员是不可能的,这意味着Card需要GetHealth

为避免强制转换,您可以将卡片类型作为数据成员存储在 Card 中,并使用非虚拟 type() 函数。然后,各种卡片类型类的构造函数将使用适当的类型构造它们的 Card 部分。

为了防止像CardMinion 等基础卡类的独立构造,并且只允许创建实际卡(如GoblinArmor 等),请保护指定类型的构造函数:

class Card

public:
    enum class Type  Minion, Spell, Equipment ;

    virtual ~Card() = default;
    Type type() const  return type_; 
    virtual int GetHealth() const  return 0; 

protected:
    explicit Card(const Type type)  type_ = type; 

private:
    Type type_;
;

class Minion : public Card

public:
    int GetHealth() const override  return mHealth; 

protected:
    Minion() : Card(Type::Minion)  

private:
    int mHealth = 100;
;

class Goblin : public Minion
 ;

class Equipment : public Card

protected:
    Equipment() : Card(Type::Equipment)  
;

class Armor : public Equipment
 ;

void test(Card& card)

    if (card.type() == Card::Type::Minion) 
        std::cout << "Health: " << card.GetHealth() << '\n';
     else 
        std::cout << "Not a minion.\n";
    


int main()

    Goblin goblin;
    Armor armor;
    test(goblin);
    test(armor);

【讨论】:

以上是关于使用纯多态性和继承从基类调用派生类上的函数而不进行强制转换?的主要内容,如果未能解决你的问题,请参考以下文章

从基类构造函数调用纯虚函数

C++派生类是不是可以从基类继承静态数据成员和静态成员函数?

如何使用多态性从基类访问派生类向量成员?

Effective C++ 6.继承与面向对象设计

从基类调用派生类函数

实验4 类的继承派生和多态