【问题标题】:Polymorphism and Dynamic Casting多态性和动态铸造
【发布时间】:2018-08-22 19:46:04
【问题描述】:

所以我正在开发基于文本的 RPG,但遇到了一个问题。我目前正在从角色的库存中装备武器。我正在努力做到这一点,以便我的程序可以判断他们想要装备的物品是否属于Weapon 类。以下是相关代码片段:

 Item tempChosenWeapon = myInventory.chooseItem();
cout << tempChosenWeapon.getName() << endl;
Item *chosenWeapon = &tempChosenWeapon;
cout << chosenWeapon->getName() << endl;//THE CODE WORKS UP TO HERE

Weapon *maybeWeapon = dynamic_cast<Weapon*>(chosenWeapon);
cout << maybeWeapon->getName() << endl;

现在,WeaponItem 的子类,这就是我使用动态强制转换的原因——试图将Item 类型的chosenWeapon 更改为输入Weapon为了比较两个类。 (我正在使用这些cout&lt;&lt;s 或测试从这些对象调用函数是否有效)。

我的程序编译了,一切运行良好,直到我们到达maybeWeapon-&gt;getName(),程序崩溃了。我已经研究了很多,但我只是不明白我做错了什么。非常感谢任何答案或替代建议!谢谢!

【问题讨论】:

  • 如果 dynamic_cast 不是武器,你认为它会做什么?
  • 铸造通常是一个设计缺陷。您可以通过使用具有不同类别项目的枚举来解决此问题,然后使用virtual getItemType() 函数返回类型。这样您就不必投射和处理所有的陷阱。
  • 我认为你不需要知道确切的类。我想你想知道的只是该项目是否可以装备。因此,另一种方法是让所有对象从Item 继承Equip() 方法。将其定义为虚函数允许 Weapon 对象以一种方式响应,并且(例如)Talisman 对象做其他事情,而基类 Equip() 什么都不做(或为用户打印提示/错误) .
  • 转换失败,你得到一个nullptr,你取消引用返回值而不检查,坏事接踵而至。

标签: c++ polymorphism game-engine software-design game-development


【解决方案1】:

问题

问题是您尝试对Weapon 进行动态转换,但实际上指向的对象是构造Item 的真实副本,而不是子类。当您取消引用它时,这会导致 nullptr 和 UB !

为什么?

假设您的库存中只有 Weapon 对象。你的 sn-p 中的第一条指令是你邪恶的根源:

    Item tempChosenWeapon = myInventory.chooseItem();

这是语句Item 对象的副本构造。如果源对象是Weapon,它将是sliced

稍后你会得到一个指向这个对象的指针:

    Item *chosenWeapon = &tempChosenWeapon;

但是这个Item* 并不像你想象的那样指向Weapon 对象。它指向一个真正粗略的Item 对象!所以当你在这里进行动态转换时:

    Weapon *maybeWeapon = dynamic_cast<Weapon*>(chosenWeapon);

代码会发现choosenWeapon 不是Weapon*dynamic_cast 的结果将是nullptr。到目前为止,这不一定是一场灾难。但是当你取消引用这个指针时,你会得到 UB:

    maybeWeapon->getName()     // OUCH !!!!!! 

解决方案

检查dynamic_cast 是否成功(即结果不是nullptr)可以防止崩溃,但不会解决您的根本问题。

问题甚至可能比预期的更深:myInventory.chooseItem() 在现实中返回什么类型?它是一个普通的 Item 吗?那么您可能已经在库存中遇到了切片问题!

如果要使用多态:

  • 您必须使用指针(最好是智能指针)或引用,以免丢失对象的原始类型,就像这里发生的那样。
  • 如果您需要复制多态对象,则不能只使用带有Item 的赋值:您需要调用多态clone() 函数并确保此克隆的目标具有兼容的类型。

从一个解决方案开始,它是这样的:

Item* chosenWeapon = myInventory.chooseItem();  // refactor choosItem() to return a pointer.
cout << chosenWeapon->getName() << endl; 
Weapon *maybeWeapon = dynamic_cast<Weapon*>(chosenWeapon);
if (maybeWeapon) 
    cout << maybeWeapon->getName() << endl;
else cout << "Oops the chosen item was not a weapon" <<endl; 

如果这仍然不起作用,那么您的库存容器将有缺陷。在这种情况下,请先查看this question,然后再用您的容器代码打开一个单独的问题

【讨论】:

  • 切片确实是问题的根源,其他答案错过了。
  • 非常感谢!但是,我在这里不明白的一件事是:为什么你有 if 语句,好像 (maybeWeapon);它似乎不应该是一个真/假的陈述......你介意详细说明吗?谢谢!
  • @TheMachoMuchacho if 语句将每个不为空的表达式都视为真。所以 if(maybeWeapon) 的行为与 if(maybeWeapon!=nullptr) 完全相同。
  • 啊,我明白了!非常感谢您的时间和帮助!
【解决方案2】:

如果无法执行指针转换,dynamic_cast 将返回 nullptr(对于引用转换,它将引发异常),因此您的代码应如下所示:

Weapon *maybeWeapon = dynamic_cast<Weapon*>(chosenWeapon);
if ( maybeWeapon  ) {
   cout << maybeWeapon->getName() << endl;
else {
   // it's not a weapon
}

如果您不执行该测试,并尝试取消引用包含 nullptr 的指针,那么您将处于未定义行为领域。

【讨论】:

    【解决方案3】:
    Item tempChosenWeapon = myInventory.chooseItem();
    

    这是Item。不是来自Item 的类型。这是一个Item

    C++ 中的值具有已知类型。

    cout << tempChosenWeapon.getName() << endl;
    

    一切都好,但请停止using namespace std;

    Item *chosenWeapon = &tempChosenWeapon;
    

    这是一个指向Item 的指针。我可以证明它不是多态的,因为它是一个指向Item 类型实例的指针。编译器可能可以证明这一点。

    cout << chosenWeapon->getName() << endl;//THE CODE WORKS UP TO HERE
    

    好的,这会重复上一次调用。

    Weapon *maybeWeapon = dynamic_cast<Weapon*>(chosenWeapon);
    

    这确定性地返回nullptrchosenWeaponItem*,我们知道它指向 Item,而 Item 不是 Weapon

    cout << maybeWeapon->getName() << endl;
    

    这取消引用nullptr


    在 C++ 中有多种处理多态性的方法。但你必须考虑一下。

    首先,你想要值语义吗?价值语义意味着某物的副本就是它的副本。事物不指代其他事物;他们就是那些东西。

    您可以使用多态值来处理值语义,但这需要一些工作。你写了两个类;值包装器和内部pImpl

    内部pImpl有一个std::unique_ptr&lt;Impl&gt; Impl-&gt;clone() const方法,复制时值包装器调用它。

    你这样写你的界面:

    template<class D>
    struct clonable {
      std::unique_ptr<D> clone() const = 0;
    };
    struct ITarget;
    struct IItem:clonable<IItem> {
      virtual std::string get_name() const = 0;
      virtual bool can_equip( ITarget const& ) const = 0;
      ~virtual IItem() {}
    };
    struct Target;
    struct Item {
      using Impl = IItem;
      explicit operator bool() const { return (bool)pImpl; }
      IItem* get_impl() { return pImpl.get(); }
      IItem const* get_impl() const { return pImpl.get(); }
      template<class D>
      D copy_and_downcast() const& {
        auto* ptr = dynamic_cast<typename D::Impl const*>( pImpl.get() );
        if (!ptr) return {};
        return D(ptr->clone());
      }
      template<class D>
      D copy_and_downcast() && {
        auto* ptr = dynamic_cast<typename D::Impl*>( pImpl.get() );
        if (!ptr) return {};
        pImpl.release();
        return D(std::unique_ptr<typename D::Impl>(ptr));
      }
      std::string get_name() const {
        if (!*this) return {};
        return pImpl->get_name();
      }
      bool can_equip(Target const& target)const{
        if (!*this) return false;
        if (!target) return false;
        return pImpl->can_equip( *target.get_impl() );
      }
      Item() = default;
      Item(Item&&) = default;
      Item& operator=(Item&&) = default;
      Item(std::unique_ptr<IItem> o):pImpl(std::move(o)) {}
      Item(Item const& o):
        Item( o?Item(o.pImpl->clone()):Item{} )
      {}
      Item& operator=( Item const& o ) {
        Item tmp(o);
        std::swap(pImpl, tmp.pImpl);
        return *this;
      }
    private:
      std::unique_ptr<IItem> pImpl;
    };
    

    这可能有错误,对你来说可能太复杂了。


    其次,您可以使用引用语义。

    在这种情况下,您希望从数据中返回 shared_ptr&lt;const T&gt;shared_ptr&lt;T&gt;。或者你可以半途而废,从你的 chooseItem 函数返回一个 unique_ptr&lt;T&gt; 副本。

    引用语义真的很难正确。但是你可以直接使用dynamic_castdynamic_pointer_cast

    std::shared_ptr<Item> chosenWeapon = myInventory.chooseItem();
    if (!chosenWeapon) return;
    std::cout << chosenWeapon->getName() << std::endl;
    
    auto maybeWeapon = dynamic_pointer_cast<Weapon>(chosenWeapon);
    if (maybeWeapon)
      std::cout << maybeWeapon->getName() << std::endl;
    else
      std::cout << "Not a weapon" << std::endl;
    

    【讨论】:

      【解决方案4】:

      您不能将Item 类型的对象强制转换为Item 子类的对象。 请注意,使用Item tempChosenWeapon = myInventory.chooseItem(),您将获得一个项目对象,即使chooseItem 可能返回一个Weapon 对象。这称为“切片”并切出任何Weapon-object 的Item-subobject。请注意,不是引用或指针的变量不是多态的:

      struct A {
          int a = 0;
          virtual void print() const {
              std::cout << "a:" << a << std::endl;
          }
      };
      
      struct B : public A {
          int b = 1;
          void print() const override {
              std::cout << "a:" << a << "; b:" << b << std::endl;
          }
      };
      
      B b;
      
      A get_b() {  // will slice b;
          return b;
      }
      
      A& getRefTo_b() {  // reference to b; polymorphic
          return b;
      }
      
      A* getPointerTo_b() {  // pointer to b; polymorphic.
          return &b;
      }
      
      int main() {
      
          A a1 = get_b(); // copy of A-subobject of b; not polymorphic
          a1.print();
          // a:0
      
          A a2 = getRefTo_b();  // copy of A-subobject of referenced b-object; not polymorphic
          a2.print();
          // a:0
      
          A &a3 = getRefTo_b(); // storing reference to b-object; polymorphic
          a3.print();
          // a:0; b:1
      
          A *a4 = getPointerTo_b();  // pointer to b-object; polymorphic
          a4->print();
          // a:0; b:1
      
          B* b1 = dynamic_cast<B*>(&a1);  // fails (nullptr); a1 is not a B
          B* b2 = dynamic_cast<B*>(&a2);  // fails (nullptr); a2 is not a B
          B* b3 = dynamic_cast<B*>(&a3);  // OK; a3 refers to a B-object
          B* b4 = dynamic_cast<B*>(a4);   // OK; a4 points to a B-object
      
          return 0;
      }
      

      所以你的签名应该是

      Item &Inventory::chooseItem() {
         static Weapon weapon;
         ...
         return weapon;
      };
      
      int main() {
         Item &myWeapon = myInventory.chooseItem();
         Weapon* w = dynamic_cast<Weapon*>(&myWeapon);
         ...
      }
      

      【讨论】:

        猜你喜欢
        • 2014-05-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多