Как добавить абстрактный базовый класс unique_ptrs на карту? - PullRequest
0 голосов
/ 28 июня 2018

Я сейчас программирую игру на C ++. Эта игра имеет класс GameManager. Класс GameManager содержит карту с указателями на игровые объекты. Я определил класс GameObject, который является абстрактным классом, действующим просто как интерфейс.

Я определил два класса, производных от класса GameObject: Enemy и Loot.

Я хочу, чтобы мой класс GameManager содержал карту игровых объектов, или, точнее, указатели на игровые объекты. Поскольку мой GameManager владеет этими объектами, я хочу, чтобы карта содержала std :: unique_ptr's.

Однако мне трудно добавить производные объекты (например, «Враг и добыча») на эту карту.

Я хочу, чтобы мой GameManager перебрал игровые объекты и вызвал абстрактные методы. По сути, мой GameManager не заботится о том, является ли что-то врагом, добычей или чем-то еще, он просто хочет иметь возможность вызывать метод «draw», как объявлено в базовом классе.

Как бы я добавил добавление unique_ptr, указывающее на производный класс, в карту, содержащую unique_ptr, в базовый класс? Мои попытки пока приводят к коду, который я не могу скомпилировать. Я получаю сообщение об ошибке, в котором говорится, что мне не разрешено динамически приводить указатель производного класса к указателю базового класса.

Мне кажется, что это нормально работает, если я использовал необработанные указатели, но я собираюсь использовать умные указатели.

Код:

#include <memory>
#include <map>

class GameObject
{
public:
    virtual void draw() = 0;
};

class  Enemy : GameObject
{
public:
    void draw() {};
};

class  Loot : GameObject
{
public:
    void draw() {};
};

int main()
{
    std::map<int, std::unique_ptr<GameObject>> my_map;

    // How do I add an Enemy or Loot object unique_ptr to this map?
    my_map[0] = dynamic_cast<GameObject>(std::unique_ptr<Enemy>().get()); // doesn't compile, complains about being unable to cast to abstract class

    return 0;
}

Ответы [ 2 ]

0 голосов
/ 28 июня 2018

Первой причиной сообщения об ошибке является то, что тип класса никогда не может использоваться в качестве типа для dynamic_cast. Целевой тип dynamic_cast всегда должен быть либо указателем на тип класса (что означает, что результатом будет нулевое значение, если приведение не выполнено), либо ссылкой на тип класса (то есть выбросить исключение, если приведение не выполнено).

Итак, улучшение №1:

my_map[0] = dynamic_cast<GameObject*>(std::unique_ptr<Enemy>().get());

Но это не сработает, потому что GameObject является частным базовым классом Enemy. Возможно, вы хотели использовать публичное наследование, но (при использовании class вместо struct) вы должны сказать так:

class Enemy : public GameObject
// ...

Далее мы обнаружим, что = в операторе map недействителен. Левая сторона имеет тип std::unique_ptr<GameObject>, который не имеет operator=, который может принимать указатель GameObject*. Но у него есть элемент reset для установки необработанного указателя:

my_map[0].reset(dynamic_cast<GameObject*>(std::unique_ptr<Enemy>().get()));

Теперь утверждение должно скомпилироваться - но оно все равно не так.

Прежде чем понять, почему это не так, мы можем сделать упрощение. dynamic_cast необходим для получения указателя на производный класс от указателя на базовый класс или для многих других изменений типа в более сложном дереве наследования. Но вообще не нужно получать указатель на базовый класс от указателя на производный класс: это допустимое неявное преобразование, поскольку каждый объект с типом производного класса всегда должен содержать подобъект типа базового класса, и нет " провал "дело. Так что dynamic_cast здесь можно просто отбросить.

my_map[0].reset(std::unique_ptr<Enemy>().get());

Следующая проблема заключается в том, что std::unique_ptr<Enemy>() создает нулевой unique_ptr, а объект Enemy вообще не создается. Чтобы создать фактический Enemy, мы можем вместо этого написать std::unique_ptr<Enemy>(new Enemy) или std::make_unique<Enemy>().

my_map[0].reset(std::make_unique<Enemy>().get());

Все еще не так, и немного хитрым способом. Теперь проблема в том, что созданный объект Enemy принадлежит временному объекту std::unique_ptr<Enemy>, возвращенному make_unique. reset сообщает std::unique_ptr<GameObject> на карте, что он должен иметь указатель на тот же объект. Но в конце оператора временный std::unique_ptr<Enemy> уничтожается и уничтожает объект Enemy. Таким образом, на карте остается указатель на мертвый объект, который недопустим для использования - не то, что вы хотели.

Но решение здесь в том, что нам вообще не нужно возиться с get() и reset(). Существует operator=, который позволяет присваивать значение std::unique_ptr<Enemy> значению std::unique_ptr<GameObject>, и он делает все правильно. Используется неявное преобразование из Enemy* в GameObject*.

my_map[0] = std::make_unique<Enemy>();

(Обратите внимание, что если у вас есть с именем std::unique_ptr<Enemy>, вам потребуется std::move, чтобы разрешить назначение, как в my_map[0] = std::move(enemy_ptr);. Но std::move выше не требуется, потому что результат из make_unique уже является значением.)

Теперь это утверждение намного короче и более разборчиво, и на самом деле будет делать то, что вы хотите.

Комментарий также предложил возможность

my_map.emplace(0, std::make_unique<Enemy>());

Это также верно, но, возможно, есть важное отличие: если на карте уже есть объект с нулевым ключом, версия = уничтожит и заменит старую, а версия emplace оставит карту в покое. и только что созданный Enemy будет уничтожен.

0 голосов
/ 28 июня 2018

dynamic_cast может использоваться только для преобразования между указателями и ссылками. GameObject не является ни типом указателя, ни ссылочным типом, поэтому вы не можете dynamic_cast к нему.

Возможно, вы намеревались вместо dynamic_cast<GameObject*>. однако вы не должны dynamic_cast указывать на базовый класс. Указатель на производный тип неявно преобразуется в указатель базового класса. Используйте static_cast, когда неявное преобразование нежелательно. Кроме того, это преобразование также невозможно, так как приведение находится вне какой-либо функции-члена и, следовательно, не может иметь доступа к закрытому базовому классу.

Кроме того, вы не можете назначить пустой указатель уникальному указателю. Чтобы передать право владения пустым указателем на уникальный указатель, вы можете использовать unique_ptr::reset. Однако вы никогда не должны хранить указатель из unique_ptr::get в другой уникальный указатель. Это приведет к неопределенному поведению, когда оба уникальных указателя деструкторов попытаются уничтожить один и тот же объект. К счастью, в этом случае указатель инициализируется значением и, следовательно, равен нулю, поэтому технически ошибка не имеет последствий. Но вы использовали нулевой указатель намеренно? Я подозреваю, что нет.

Вставить уникальный указатель на производный объект в карту уникальных указателей на базу очень просто. Пусть ptr будет уникальным указателем на Enemy:

std::unique_ptr<Enemy> ptr = get_unique_pointer_from_somewhere();

Просто переместите присвоить уникальный указатель:

my_map[0] = std::move(ptr);

Или вы можете использовать emplace функцию-член карты.

Наконец, деструктор unique_ptr<GameObject> будет иметь неопределенное поведение, если он указывает на производный объект. Чтобы исправить, объявите деструктор GameObject virtual.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...