Подтвердить что ты не робот

С++ 11 Смарт-указатель Smart Pointer

Я работаю с указателями уже несколько лет, но я только недавно решил перейти на интеллектуальные указатели С++ 11 (а именно, уникальный, общий и слабый). Я провел довольно много исследований по ним, и вот те выводы, которые я сделал:

  • Уникальные указатели замечательные. Они управляют своей собственной памятью и являются такими же легкими, как и исходные указатели. Предпочитайте unique_ptr над необработанными указателями как можно больше.
  • Общие указатели сложны. Они имеют значительные накладные расходы из-за подсчета ссылок. Передайте их по ссылке const или пожалеете об ошибках ваших путей. Они не злые, но их следует использовать экономно.
  • Общие указатели должны иметь объекты; используйте слабые указатели, когда владение не требуется. Блокировка weak_ptr имеет эквивалентные служебные данные для конструктора копии shared_ptr.
  • Продолжайте игнорировать существование auto_ptr, которое теперь устарело.

Итак, имея в виду эти принципы, я отправился пересматривать свою базу кода, чтобы использовать наши новые блестящие умные указатели, полностью намереваясь очистить доски как можно большего количества исходных указателей. Однако я запутался в том, как лучше использовать интеллектуальные указатели С++ 11.

Предположим, например, что мы разрабатывали простую игру. Мы решаем, что оптимально загружать вымышленный тип данных Texture в класс TextureManager. Эти текстуры сложны, поэтому их невозможно реализовать по значению. Более того, предположим, что игровым объектам нужны конкретные текстуры в зависимости от их типа объекта (например, автомобиль, лодка и т.д.).

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

Итак, мой вопрос заключается в том, как наилучшим образом использовать интеллектуальные указатели в этой ситуации. Я вижу несколько вариантов:

  • Храните текстуры непосредственно в контейнере, а затем создайте unique_ptr в каждом игровом объекте.

    class TextureManager {
      public:
        const Texture& texture(const std::string& key) const
            { return textures_.at(key); }
      private:
        std::unordered_map<std::string, Texture> textures_;
    };
    class GameObject {
      public:
        void set_texture(const Texture& texture)
            { texture_ = std::unique_ptr<Texture>(new Texture(texture)); }
      private:
        std::unique_ptr<Texture> texture_;
    };
    

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

  • Храните не текстуры напрямую, а их общие указатели в контейнере. Используйте make_shared для инициализации общих указателей. Построить слабые указатели в игровых объектах.

    class TextureManager {
      public:
        const std::shared_ptr<Texture>& texture(const std::string& key) const
            { return textures_.at(key); }
      private:
        std::unordered_map<std::string, std::shared_ptr<Texture>> textures_;
    };
    class GameObject {
      public:
        void set_texture(const std::shared_ptr<Texture>& texture)
            { texture_ = texture; }
      private:
        std::weak_ptr<Texture> texture_;
    };
    

    В отличие от случая unique_ptr, мне не придется копировать-строить сами текстуры, но рендеринг игровых объектов дорог, так как мне пришлось бы блокировать weak_ptr каждый раз (сложнее, чем копирование нового shared_ptr).

Итак, чтобы понять, мое понимание таково: если бы я использовал уникальные указатели, мне пришлось бы копировать текстуры; альтернативно, если бы я использовал общие и слабые указатели, мне пришлось бы по существу копировать общие указатели каждый раз, когда игровой объект должен быть нарисован.

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

Может ли кто-нибудь указать мне правильное направление?

Извините за долгое чтение и спасибо за ваше время!

4b9b3361

Ответ 1

Даже в С++ 11 необработанные указатели по-прежнему отлично действуют как ссылки, не относящиеся к объектам. В вашем случае вы говорите: "Пусть предполагается, что текстуры гарантированно переживут свои указатели". Это означает, что вы совершенно безопасны для использования исходных указателей на текстуры в игровых объектах. Внутри менеджера текстур сохраняйте текстуры либо автоматически (в контейнере, который гарантирует постоянное расположение в памяти), либо в контейнере unique_ptr s.

Если гарантия отложенного указателя была не действительна, имеет смысл хранить текстуры в shared_ptr в менеджере и использовать либо shared_ptr или weak_ptr в игровых объектах, в зависимости от семантики владения игровыми объектами в отношении текстур. Вы можете даже отменить это: сохранить shared_ptr в объектах и ​​weak_ptr в менеджере. Таким образом, менеджер будет служить кешем - если запрошенная текстура и ее weak_ptr все еще действительны, она выдаст ее копию. В противном случае он загрузит текстуру, выдаст shared_ptr и сохранит weak_ptr.

Ответ 2

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

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

Один из способов сделать это - вернуть TextureManager текстуру const *. Рассмотрим:

using TextureRef = Texture const*;
...
TextureRef TextureManager::texture(const std::string& key) const;

Поскольку объект underling Texture имеет срок службы вашего приложения, он никогда не изменяется и всегда существует (ваш указатель никогда не имеет значения nullptr), вы можете просто обрабатывать TextureRef как простое значение. Вы можете передать их, вернуть их, сравнить их и сделать из них контейнеры. Их очень легко рассуждать и очень эффективно работать.

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

TextureRef t{texture_manager.texture("grass")};

// You can treat t as a value. You can pass it, return it, compare it,
// or put it in a container.
// But you use it like a pointer.

double aspect_ratio{t->get_aspect_ratio()};

Один из способов справиться с этим - использовать что-то вроде идиомы pimpl и создать класс, который является не чем иным, как оболочкой для указателя на реализацию текстуры. Это немного больше, потому что вы создадите API (функции-члены) для вашего класса оболочки текстур, которые переходят на ваш API класса реализации. Но преимущество в том, что у вас есть класс текстуры с семантикой значения и синтаксисом значений.

struct Texture
{
  Texture(std::string const& texture_name):
    pimpl_{texture_manager.texture(texture_name)}
  {
    // Either
    assert(pimpl_);
    // or
    if (not pimpl_) {throw /*an appropriate exception */;}
    // or do nothing if TextureManager::texture() throws when name not found.
  }
  ...
  double get_aspect_ratio() const {return pimpl_->get_aspect_ratio();}
  ...
  private:
  TextureImpl const* pimpl_; // invariant: != nullptr
};

...

Texture t{"grass"};

// t has both value semantics and value syntax.
// Treat it just like int (if int had member functions)
// or like std::string (except lighter weight for copying).

double aspect_ratio{t.get_aspect_ratio()};

Я предполагал, что в контексте вашей игры вы никогда не попросите текстуру, которая не гарантируется. Если это так, вы можете просто утверждать, что имя существует. Но если это не так, то вам нужно решить, как справиться с этой ситуацией. Моя рекомендация состояла бы в том, чтобы сделать его инвариантом вашего класса-оболочки, что указатель не может быть nullptr. Это означает, что вы выкидываете из конструктора, если текстура не существует. Это означает, что вы справляетесь с проблемой при попытке создать текстуру, вместо того, чтобы проверять нулевой указатель каждый раз, когда вы вызываете члена вашего класса-оболочки.

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

Ответ 3

У вас может быть std:: map std:: unique_ptrs, где хранятся текстуры. Затем вы можете написать метод get, который возвращает ссылку на текстуру по имени. Таким образом, если каждая модель знает название своей текстуры (что она должна), вы можете просто передать имя в метод get и получить ссылку с карты.

class TextureManager 
{
  public:
    Texture& get_texture(const std::string& key) const
        { return *textures_.at(key); }
  private:
    std::unordered_map<std::string, std::unique_ptr<Texture>> textures_;
};

Затем вы можете просто использовать текстуру в классе игрового объекта, а не Texture *, weak_ptr и т.д.

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

Ответ 4

Прежде чем я пойду, как я случайно роман...

TL; DR Используйте общие указатели для определения вопросов ответственности, но будьте очень осторожны в циклических отношениях. Если бы я был вами, я бы использовал таблицу общих указателей для хранения ваших активов, и все, что нужно этим общим указателям, также должно использовать общий указатель. Это устраняет накладные расходы слабых указателей для чтения (поскольку эти накладные расходы в игре похожи на создание нового умного указателя 60 раз в секунду на объект). Это также подход, который мы использовали для моей команды, и это было суперэффективно. Вы также говорите, что ваши текстуры гарантированно переживут объекты, поэтому ваши объекты не могут удалить текстуры, если они используют общие указатели.

Если бы я мог бросить свои 2 цента, я хотел бы рассказать вам о почти одинаковом набеге, который я взял с помощью умных указателей в своей собственной видеоигре; как хорошее, так и плохое.

Этот код игры использует почти идентичный подход к вашему решению # 2: таблица, заполненная интеллектуальными указателями на растровые изображения.

У нас были некоторые отличия; мы решили разбить нашу таблицу растровых изображений на 2 части: один для "срочных" растровых изображений и один для "простых" растровых изображений. Срочные растровые изображения - это растровые изображения, которые постоянно загружаются в память и будут использоваться в середине битвы, где нам понадобилась анимация "СЕЙЧАС" и не хотелось идти на жесткий диск, который имел очень заметное заикание. Таблица facile была таблицей строк путей файла к растровым изображениям на hdd. Это были бы большие битмапы, загруженные в начале относительно длинного участка геймплей; как ваша анимационная анимация персонажа или фоновое изображение.

Использование исходных указателей здесь имеет некоторые проблемы, в частности, право собственности. См., Наша таблица активов имела функцию Bitmap *find_image(string image_name). Эта функция сначала проверит таблицу сроков для записи, соответствующей image_name. Если будет найдено, отлично! Верните указатель растрового изображения. Если вы не нашли, выполните поиск в таблице. Если мы найдем путь, соответствующий вашему имени изображения, создайте растровое изображение, а затем верните этот указатель.

Класс для использования этого наиболее определенно был нашим классом анимации. Здесь проблема собственности: когда анимация должна удалять ее растровое изображение? Если бы это было на простом столе, тогда проблем не было; это растровое изображение было создано специально для вас. Это ваш долг - удалить его!

Однако, если ваше растровое изображение произошло из таблицы срочных действий, вы не можете удалить его, так как это не позволит другим использовать его, а ваша программа будет уменьшаться, как у E.T. игра, и ваши продажи следуют примеру.

Без интеллектуальных указателей единственное решение здесь состоит в том, чтобы класс анимации клонировал свои растровые изображения независимо от того, что. Это позволяет безопасно удалять, но убивает скорость программы. Разве это изображение не должно быть чувствительным к времени?

Однако, если класс активов должен был вернуть shared_ptr<Bitmap>, вам не о чем беспокоиться. Таблица наших активов была статичной, и вы видите, что эти указатели продолжаются до конца программы, несмотря ни на что. Мы изменили нашу функцию на shared_ptr<Bitmap> find_image (string image_name) и никогда не должны были снова клонировать растровое изображение. Если растровое изображение появилось из таблицы, то этот умный указатель был единственным в своем роде и был удален с помощью анимации. Если это было срочное растровое изображение, тогда таблица все еще содержала ссылку на уничтожение анимации, и данные были сохранены.

Это счастливая часть, здесь уродливая часть.

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

Видите, у нас была утечка памяти, и мы подумали: "Мы должны использовать интеллектуальные указатели везде!". Огромная ошибка.

В нашей игре было GameObjects, которое контролировалось Environment. Каждая среда имела вектор GameObject * 's, и каждый объект имел указатель на свою среду.

Вы должны увидеть, где я собираюсь с этим.

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

Если среда была единственным держателем ссылки на объект, то ваш объект не мог покинуть среду без удаления. Обычно это происходит при создании снарядов, особенно для телепортации снарядов.

Объекты также удаляли свою среду, по крайней мере, если они были последними, чтобы оставить ее. Среда для большинства игровых состояний также была конкретным объектом. МЫ ЗВОЛИТЕ УДАЛИТЬ НА СТЕКЕ! Да, мы были любителями, судим нас.

По моему опыту, используйте unique_pointers, когда вы слишком ленивы для вызова delete, и только одна вещь когда-либо будет владеть вашим объектом, используйте shared_pointers, когда вы хотите, чтобы несколько объектов указывали на одну вещь, но не могут решить, кто должен удалить он, и очень осторожно относитесь к циклическим отношениям с shared_pointers.