Итак, я работаю над текстовой 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;
Теперь Weapon
является дочерним классом Item
, поэтому я использую динамическое приведение - в попытке изменить chosenWeapon
, который имеет тип Item
, на тип Weapon
, чтобы сравнить два класса. (Я использую эти cout<<
, чтобы проверить, работает ли вызов функции из этих объектов).
Моя программа компилируется, и все работает нормально, пока мы не дойдем до maybeWeapon->getName()
, где программа вылетает. Я довольно много исследовал, но просто не понимаю, что делаю не так. Любой ответ или альтернативное предложение приветствуются! Спасибо!
литье обычно является недостатком конструкции. Вы можете решить эту проблему, используя перечисление с различными классами элементов, которые у вас могут быть, а затем иметь функцию virtual getItemType()
, которая возвращает тип. Таким образом, вам не придется бросать и разбираться со всеми подводными камнями.
Я думаю, вам не нужно знать точный класс. Думаю, все, что вам нужно знать, - это можно ли надеть этот предмет. Итак, один из альтернативных подходов состоит в том, чтобы все объекты унаследовали метод Equip()
от Item
. Определение этого как виртуальной функции позволяет объектам Weapon
отвечать одним способом и (скажем) объектам Talisman
делать что-то еще, в то время как базовый класс Equip()
ничего не делает (или выводит подсказку / ошибку для пользователя).
Сбой при трансляции, вы получаете nullptr
, вы разыменовываете возвращаемое значение без проверки, в результате возникают плохие вещи.
Рассмотрите возможность добавления полиморфного метода is_weapon (), который возвращает bool. Производные классы уникальным образом реализуют собственный ответ в одной строке кода. Чтобы уменьшить усилия, все неоружие может использовать определенный базовый класс is_weapon (), который возвращает false. Возможно, у вашего оружия есть оценки (танк и т. д.) Is_weapon () может вернуть перечисление оценок. не-оружие было бы приемлемо, но явно не оружие. Так много способов избежать динамического приведения. Я согласен с Натаном, кастинг - это недостаток дизайна.
dynamic_cast вернет nullptr, если приведение указателя не может быть выполнено (для приведений ссылок оно вызовет исключение), поэтому ваш код должен читать что-то вроде:
Weapon *maybeWeapon = dynamic_cast<Weapon*>(chosenWeapon);
if ( maybeWeapon ) {
cout << maybeWeapon->getName() << endl;
else {
// it's not a weapon
}
Если вы не выполните этот тест и попытаетесь разыменовать указатель, содержащий nullptr, вы отключитесь от Undefined Behavior Land.
Проблема в том, что вы пытаетесь выполнить динамическое приведение к 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;
Если это все еще не сработает, значит, ваш инвентарный контейнер неисправен. В этом случае посмотрите этот вопрос, прежде чем открывать отдельный вопрос с кодом вашего контейнера.
Срез действительно является корнем проблемы, который упускается из виду.
Большое спасибо! Однако одного я не понимаю здесь: почему у вас есть оператор if, как если бы (возможно, Weapon); не похоже, что здесь должно быть истинное / ложное утверждение ... не могли бы вы уточнить? Спасибо!
@TheMachoMuchacho оператор if принимает за истину каждое выражение, которое не является нулевым. Итак, if (mightWeapon) будет вести себя точно так же, как if (mightWeapon! = 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);
Это детерминированно возвращает nullptr
. chosenWeapon
- это Item*
, который, как мы знаем, указывает на Item
, а 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>
из ваших данных. Или вы можете пойти на полпути и вернуть копию unique_ptr<T>
из ваших функций chooseItem
.
Эталонную семантику действительно сложно понять. Но вы можете использовать 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, даже если chooseItem
может вернуть объект Weapon
. Это называется «нарезкой» и вырезает подобъект Item
из любого объекта Weapon
. Обратите внимание, что переменные, которые не являются ссылками или указателями, не являются полиморфными:
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);
...
}
как вы думаете, что делает dynamic_cast, если это не оружие?