С++ Вызов функции дочернего класса из базового класса, когда я не знаю дочерний тип

У меня есть инвентарь, в котором хранятся файлы InventoryItem.

struct InventoryItem{
Item* item;
unsigned int quantity;};

std::vector<InventoryItem> m_items;

Я добавляю такие элементы, как следующие, m_inventory.addItem(bandage);

Но когда я пытаюсь вызвать функцию use() Bandages, которая была получена из базового класса Item, вместо этого вызывается функция use() класса Item.

Он был объявлен в классе Item следующим образом:

   // ....
public:
   // ....
    virtual void use(Player&){}
   // ....

Он был объявлен в классе Bandage следующим образом:

   // ....
class Bandage : public Item{
public:
   // ....
    virtual void use(Player&);
   // ....

Он был определен в классе Bandage следующим образом:

void Bandage::use(Player& player)
{
    player.heal(35);
} 

Когда я пытаюсь вызвать функцию use() моего элемента, скажем, например, m_items.at(i).item->use(*m_player);

Он вызывает функцию базового класса «Item» use(), а не функцию «Bandage» use().

РЕДАКТИРОВАТЬ: вот моя функция addItem,

void Inventory::addItem(const Item& item)
{
    if ((m_items.size() < m_capacity))
    {
        bool foundItem(false);
        for (auto invItem : m_items)
        {
            // if the item already exists, lets just increase the quantity
            if (invItem.item->getID() == item.getID())
            {
                foundItem = true;
                if (invItem.quantity < invItem.item->getInventoryQuantityLimit())
                {
                    ++invItem.quantity;
                }
            }
        }

        if (!foundItem){
            m_items.push_back(InventoryItem{ new Item(item), 1 });
        }

    }
}

person Jack H    schedule 05.04.2015    source источник
comment
Можете ли вы опубликовать минимальный компилируемый пример? Непонятно, что не так. Попробуйте сократить код до 2 классов, базового и производного, и опубликуйте его здесь. Возможно, это связано с вашей функцией addItem.   -  person vsoftco    schedule 05.04.2015
comment
Хорошо, я показал функцию addItem в своем посте. Потеряю ли я доступ к полученной информации, если передам константную ссылку, а не указатель?   -  person Jack H    schedule 05.04.2015
comment
Нет, виртуальные механизмы одинаково хорошо работают и для ссылок. Смотрите мой обновленный ответ, чтобы увидеть, в чем (я думаю) проблема.   -  person vsoftco    schedule 05.04.2015
comment
В дополнение к тому, что в ответах, ваш std::vector<InventoryItem> m_items; неверен: присвоение записи даже правильного типа приведет к ее сокращению до InventoryItem. На самом деле у вас не может быть вектора ~ любого производного типа ~, компилятор понятия не имеет, как его выделить. Что вам нужно сделать, так это сохранить указатели на них - все указатели на структуры одинаковы, поэтому указатель на повязку работает просто отлично, и доступ к его базе инвентаря через этот указатель, конечно, также работает.   -  person jthill    schedule 05.04.2015


Ответы (4)


Проблема в вашей функции addItem(), точнее здесь:

m_items.push_back(InventoryItem{ new Item(item), 1 });

Указатель Item* в InventoryItem в конечном итоге указывает на Item, а не на объекты, производные от Item, даже если item является производным объектом. Выражение new Item(item) не создает производный объект, а вызывает (по умолчанию, если вы его не написали) конструктор копирования Item с производным объектом item в качестве параметра. Затем он создает объект Item в куче, возвращая указатель на него.

Вам будет лучше использовать фабричный метод, создающий требуемые элементы, и избавиться от необработанных указателей в пользу std::shared_ptr (или std::unique_ptr, хотя, наверное, здесь std::shared_ptr лучше).

person vsoftco    schedule 05.04.2015

Как уже было сказано, строка new Item(item) создает базовый (Item), а не производный объект.

Если вы действительно хотите клонировать свой объект, а не просто сохранять предоставленный указатель (например, лучше использовать auto_ptr), рассмотрите возможность добавления функции clone() к вашему Item и производным классам:

class Item {
    ...
    virtual Item* clone() { return new Item(*this);}
}

class Bandage : public Item {
    ...
    virtual Bandage* clone() { return new Bandage(*this); }
}

....
m_items.push_back(InventoryItem{ item.clone(), 1 });
person Petr    schedule 05.04.2015
comment
вы можете захотеть сделать это virtual и использовать ковариантный возвращаемый тип (т.е. virtual Bandage* clone() { return new Bandage(*this);} Хотя ковариантный возвращаемый тип здесь строго не требуется. - person vsoftco; 05.04.2015
comment
Спасибо, не знал, что можно изменить тип возвращаемого значения в переопределенной виртуальной функции, как это. - person Petr; 05.04.2015
comment
Итак, я быстро попробовал это - и это работает. Но я думаю, что я собираюсь попробовать фабричный метод, таким образом, у меня будет только один производный элемент каждого типа? - person Jack H; 05.04.2015
comment
@Petr, я думаю, что это единственное исключение (работает также со ссылками), когда вы действительно можете, и, вероятно, было разработано специально для clone-подобных функций, поэтому вы избегаете ненужных приведений позже, а также имеете лучший тип времени компиляции проверка. - person vsoftco; 05.04.2015
comment
@JackH, что ты имеешь в виду - есть только по одному каждого типа? - person Petr; 05.04.2015
comment
Вместо создания клона, могу ли я просто создать все предметы и сохранить их, а затем просто передать ссылку на предмет, если его нужно добавить в инвентарь? РЕДАКТИРОВАТЬ: На самом деле я только что заметил утнапистный ответ. Поскольку предмет не имеет состояния, и есть только один инвентарь и один игрок, я думаю, что такой подход был бы уместным. - person Jack H; 05.04.2015

Трудно понять, что может быть не так, но, возможно, использование auto в вашем цикле все портит. В качестве быстрого теста вы можете попробовать добавить статическое приведение, чтобы убедиться, что auto не портит разрешение виртуальной функции.

person pabloxrl    schedule 05.04.2015

Здесь есть два возможных решения:

Во-первых, если предметы не имеют состояния, вы можете реализовать это так, чтобы в игре был только один предмет каждого типа, и делиться им со всеми игроками (если предметы не имеют состояния, то игрок, использующий предмет, повлияет на состояние инвентаря для других игроков или вызовет неопределенное поведение при использовании в разных потоках).

Изменения кода (для реализации элементов без сохранения состояния):

struct InventoryItem{
    std::shared_ptr<Item> item; // all inventory items using
                                // this (stateless) item, will share ownership
    unsigned int quantity;
};

void Inventory::addItem(const Item& item)
{
    if ((m_items.size() < m_capacity))
    {
        // everything here is the same as your code

        if (!foundItem){
            m_items.emplace_back( &item, 1 ); // share item
        }
    }
}

Во-вторых, если элементы не имеют состояния, вам потребуется метод виртуального клонирования (позволяющий каждой специализации клонировать свой собственный специализированный тип.

Изменения реализации:

class item {
    // rest of class here ...
public:
    virtual std::shared_ptr<item> clone() const = 0;
};

class bandage {
public:
    std::shared_ptr<item> clone() const override
    {
        return std::shared_ptr<item>{ new bandage{ *this } }; // use bandage copy constructor
                                     // and instantiate concrete item type
    }
};

void Inventory::addItem(const Item& item)
{
    if ((m_items.size() < m_capacity))
    {
        // everything here is the same as your code

        if (!foundItem){
            m_items.emplace_back( item.clone(), 1 ); // create specialized type
                                                  // through clone
        }
    }
}
person utnapistim    schedule 05.04.2015
comment
Работает ли emplace_back для агрегатов? В любом случае вам нужно &item при установке обратно (первый фрагмент кода), когда вы передаете указатель. - person vsoftco; 05.04.2015
comment
@vsoftco, исправлено; Спасибо. - person utnapistim; 05.04.2015
comment
Я по-прежнему не думаю, что emplace_back работает, см. вживую coliru.stacked-crooked.com/a/6f58c43268218030 - person vsoftco; 05.04.2015