多态性和动态转换
Polymorphism and Dynamic Casting
所以我正在开发一个基于文本的角色扮演游戏,我 运行 遇到了一个问题。我目前正致力于从角色的库存中装备武器。我正在努力做到这一点,以便我的程序可以判断他们想要装备的物品是否属于 class 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;
现在,Weapon
是 Item
的子 class,这就是我使用动态转换的原因——试图改变 chosenWeapon
,这是类型 Item
,输入 Weapon
以比较两个 class。 (我在 or 中使用这些 cout<<
s 来测试从这些对象调用函数是否有效)。
我的程序可以编译,一切 运行 都很好,直到我们到达 maybeWeapon->getName()
,程序崩溃。我已经研究了很多,但我只是不明白我做错了什么。非常感谢任何答案或替代建议!谢谢!
如果无法执行指针转换,dynamic_cast 将 return nullptr(对于引用转换,它将抛出异常),因此您的代码应如下所示:
Weapon *maybeWeapon = dynamic_cast<Weapon*>(chosenWeapon);
if ( maybeWeapon ) {
cout << maybeWeapon->getName() << endl;
else {
// it's not a weapon
}
如果您不执行该测试,并尝试取消对包含 nullptr 的指针的引用,那么您就离开了未定义行为领域。
问题
问题是您尝试对 Weapon
进行动态转换,但实际上指向的对象是构造的真实副本 Item
而不是子类。当您取消引用时,这会导致 nullptr
和 UB!
为什么?
假设您的库存中只有 Weapon
件物品。代码段中的第一条指令是罪恶的根源:
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()
return是什么类型的?它是一个普通的 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,然后再用您的容器代码打开一个单独的问题
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);
这确定性地 returns nullptr
。 chosenWeapon
是我们知道指向 Item
的 Item*
,而 Item
不是 Weapon
。
cout << maybeWeapon->getName() << endl;
这会取消引用 nullptr
。
在 C++ 中有多种处理多态性的方法。但是你得想一想。
首先,你想要值语义吗?值语义意味着某物的副本就是它的副本。事物不指代其他事物;他们就是那些东西。
您可以使用多态值来处理值语义,但这需要一些工作。你写两个类;值包装器和内部 pImpl
.
内部pImpl
有一个std::unique_ptr<Impl> Impl->clone() const
方法,value wrapper在复制的时候会调用它。
你这样写你的界面:
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;
};
这可能有错误,对您来说可能太复杂了。
其次,您可以使用引用语义。
在这种情况下,您希望从您的数据中 return shared_ptr<const T>
或 shared_ptr<T>
。或者你可以半途而废 return 从你的 chooseItem
函数中复制 unique_ptr<T>
。
引用语义真的很难搞定。但是您可以直接使用 dynamic_cast
或 dynamic_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-object,即使 chooseItem
可能 return 一个 Weapon
-object。这称为 "slicing" 并剪切出任何 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);
...
}
所以我正在开发一个基于文本的角色扮演游戏,我 运行 遇到了一个问题。我目前正致力于从角色的库存中装备武器。我正在努力做到这一点,以便我的程序可以判断他们想要装备的物品是否属于 class 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;
现在,Weapon
是 Item
的子 class,这就是我使用动态转换的原因——试图改变 chosenWeapon
,这是类型 Item
,输入 Weapon
以比较两个 class。 (我在 or 中使用这些 cout<<
s 来测试从这些对象调用函数是否有效)。
我的程序可以编译,一切 运行 都很好,直到我们到达 maybeWeapon->getName()
,程序崩溃。我已经研究了很多,但我只是不明白我做错了什么。非常感谢任何答案或替代建议!谢谢!
dynamic_cast 将 return nullptr(对于引用转换,它将抛出异常),因此您的代码应如下所示:
Weapon *maybeWeapon = dynamic_cast<Weapon*>(chosenWeapon);
if ( maybeWeapon ) {
cout << maybeWeapon->getName() << endl;
else {
// it's not a weapon
}
如果您不执行该测试,并尝试取消对包含 nullptr 的指针的引用,那么您就离开了未定义行为领域。
问题
问题是您尝试对 Weapon
进行动态转换,但实际上指向的对象是构造的真实副本 Item
而不是子类。当您取消引用时,这会导致 nullptr
和 UB!
为什么?
假设您的库存中只有 Weapon
件物品。代码段中的第一条指令是罪恶的根源:
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()
return是什么类型的?它是一个普通的 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,然后再用您的容器代码打开一个单独的问题
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);
这确定性地 returns nullptr
。 chosenWeapon
是我们知道指向 Item
的 Item*
,而 Item
不是 Weapon
。
cout << maybeWeapon->getName() << endl;
这会取消引用 nullptr
。
在 C++ 中有多种处理多态性的方法。但是你得想一想。
首先,你想要值语义吗?值语义意味着某物的副本就是它的副本。事物不指代其他事物;他们就是那些东西。
您可以使用多态值来处理值语义,但这需要一些工作。你写两个类;值包装器和内部 pImpl
.
内部pImpl
有一个std::unique_ptr<Impl> Impl->clone() const
方法,value wrapper在复制的时候会调用它。
你这样写你的界面:
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;
};
这可能有错误,对您来说可能太复杂了。
其次,您可以使用引用语义。
在这种情况下,您希望从您的数据中 return shared_ptr<const T>
或 shared_ptr<T>
。或者你可以半途而废 return 从你的 chooseItem
函数中复制 unique_ptr<T>
。
引用语义真的很难搞定。但是您可以直接使用 dynamic_cast
或 dynamic_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-object,即使 chooseItem
可能 return 一个 Weapon
-object。这称为 "slicing" 并剪切出任何 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);
...
}