Полиморфизм и динамическое литье

Итак, я работаю над текстовой 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(), где программа вылетает. Я довольно много исследовал, но просто не понимаю, что делаю не так. Любой ответ или альтернативное предложение приветствуются! Спасибо!

как вы думаете, что делает dynamic_cast, если это не оружие?

pm100 22.08.2018 21:48
en.cppreference.com/w/cpp/language/dynamic_cast
juanchopanza 22.08.2018 21:48

литье обычно является недостатком конструкции. Вы можете решить эту проблему, используя перечисление с различными классами элементов, которые у вас могут быть, а затем иметь функцию virtual getItemType(), которая возвращает тип. Таким образом, вам не придется бросать и разбираться со всеми подводными камнями.

NathanOliver 22.08.2018 21:50

Я думаю, вам не нужно знать точный класс. Думаю, все, что вам нужно знать, - это можно ли надеть этот предмет. Итак, один из альтернативных подходов состоит в том, чтобы все объекты унаследовали метод Equip() от Item. Определение этого как виртуальной функции позволяет объектам Weapon отвечать одним способом и (скажем) объектам Talisman делать что-то еще, в то время как базовый класс Equip() ничего не делает (или выводит подсказку / ошибку для пользователя).

Tim Randall 22.08.2018 21:56

Сбой при трансляции, вы получаете nullptr, вы разыменовываете возвращаемое значение без проверки, в результате возникают плохие вещи.

Jesper Juhl 22.08.2018 22:10

Рассмотрите возможность добавления полиморфного метода is_weapon (), который возвращает bool. Производные классы уникальным образом реализуют собственный ответ в одной строке кода. Чтобы уменьшить усилия, все неоружие может использовать определенный базовый класс is_weapon (), который возвращает false. Возможно, у вашего оружия есть оценки (танк и т. д.) Is_weapon () может вернуть перечисление оценок. не-оружие было бы приемлемо, но явно не оружие. Так много способов избежать динамического приведения. Я согласен с Натаном, кастинг - это недостаток дизайна.

2785528 22.08.2018 22:17
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
6
390
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

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; 

Если это все еще не сработает, значит, ваш инвентарный контейнер неисправен. В этом случае посмотрите этот вопрос, прежде чем открывать отдельный вопрос с кодом вашего контейнера.

Срез действительно является корнем проблемы, который упускается из виду.

Jeffrey 22.08.2018 22:24

Большое спасибо! Однако одного я не понимаю здесь: почему у вас есть оператор if, как если бы (возможно, Weapon); не похоже, что здесь должно быть истинное / ложное утверждение ... не могли бы вы уточнить? Спасибо!

Little Boy Blue 23.08.2018 03:00

@TheMachoMuchacho оператор if принимает за истину каждое выражение, которое не является нулевым. Итак, if (mightWeapon) будет вести себя точно так же, как if (mightWeapon! = Nullptr).

Christophe 23.08.2018 07:21

Ах я вижу! Что ж, большое спасибо за ваше время и вашу помощь!

Little Boy Blue 23.08.2018 15:04
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);
   ...
}

Другие вопросы по теме