多态性和动态铸造



所以我正在开发一个基于文本的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的子类,这就是为什么我使用动态强制转换 - 试图将类型ItemchosenWeapon更改为类型Weapon以比较两个类。(我正在使用这些cout<<来测试从这些对象调用函数是否有效)。

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

问题

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

为什么?

假设您的物品栏中只有Weapon对象。 你的代码段中的第一个指令是你邪恶的根源:

Item tempChosenWeapon = myInventory.chooseItem();

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

稍后,您将获取指向此对象的指针:

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的赋值:您需要调用多态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; 

如果这仍然不起作用,那么您的库存容器将有缺陷。 在这种情况下,请在使用容器代码打开单独的问题之前查看此问题

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

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

如果您不执行该测试,并尝试取消引用包含 nullptr 的指针,那么您将进入未定义的行为状态。

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

这将确定地返回nullptrchosenWeapon是我们知道指向ItemItem*,而Item不是Weapon

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

这会取消引用nullptr

有许多

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

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

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

内部pImpl有一个std::unique_ptr<Impl> Impl->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<const T>shared_ptr<T>。 或者,您可以半途而废,从chooseItem函数中返回unique_ptr<T>副本。

引用语义真的很难正确。 但是您确实可以直接使用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;

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

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

相关内容

  • 没有找到相关文章

最新更新