Должен ли я называть reset на слабом_ptr, если я заметил, что оно истекло?

У меня есть коллекция объектов Creature, которые создаются и принадлежат одной части моего приложения, используя std::make_shared и std::shared_ptr.

Я также отслеживаю выбор нуля или одного Creature в объекте World, используя std::weak_ptr<Creature>.

void World::SetSelection(const std::shared_ptr<Creature>& creature) {
    selection = creature;
}

std::shared_ptr<Creature> World::GetSelection() const {
    return selection.lock();
}

Вызывающий GetSelection отвечает за проверку пустого указателя. Если это так, значит, в настоящее время нет выбора.

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

Однако в этом случае член World::selection все еще указывает на блок управления std::shared_ptr. Это может быть довольно большим, потому что я использую std::make_shared для создания объектов Creature (я понимаю, что объект Creature был правильно уничтожен в нужное время, но память для него все еще выделена). Я рассматриваю возможность изменения GetSelection на следующее:

std::shared_ptr<Creature> World::GetSelection() {
    const auto ret = selection.lock();
    if (!ret)
        selection.reset();

    return ret;
}

Это освобождает память, как только я замечаю, что это больше не нужно. Раздражающе, эта версия GetSelection не может быть const.

Мои вопросы:

  • Какую версию GetSelection можно считать лучшей практикой в ​​этой ситуации?

  • Изменяется ли ответ, если что-то подобное происходит в шаблоном коде, где sizeof(T) неизвестно и может быть огромным? Или в С++ 14, где std::make_shared<T[]> может быть задействован?

  • Если вторая версия всегда лучшая, в чем смысл std::weak_ptr<T>::expired и lock не делать это сами?

Ответ 1

Прежде всего следует отметить, что стратегия размещения std::make_shared является необязательной, то есть стандарт не требует, чтобы реализации выполняли эту оптимизацию. Это необязательное требование, а это означает, что идеально соответствующие реализации могут решить отказаться от него.

Чтобы ответить на ваши вопросы:

  • Учитывая, что у вас, кажется, есть только один выбор (и вы поэтому не раздуваете ваше использование памяти, сохраняя многие из этих блоков управления), я бы сказал, что это просто. Является ли память узким местом? Это кричит микро-оптимизация для меня. Вы должны написать более простой код, где вы можете применить const, а затем вернуться и оптимизировать позже, если возникнет такая необходимость.

  • Ответ не безоговорочно меняется, он меняет условия на проблемную область и то, что ваше узкое место. Если вы выделяете один объект, который "огромный" (скажем, сто килобайт), и пространство для этого объекта начинает опускаться в относительно неиспользованном блоке управления до замены, это, вероятно, не является вашим узким местом и, вероятно, не стоит написание большего количества кода (что по своей сути более подвержено ошибкам, сложно поддерживать и расшифровывать), чтобы "решить".

  • Поскольку std::weak_ptr::lock и std::weak_ptr::expired являются const, при интерпретации const для С++ 11 они должны быть потокобезопасными. Поэтому, учитывая некоторые std::weak_ptr, должно быть безопасным одновременное вызов любой комбинации lock() и expired(). Под капотом std::weak_ptr хранится указатель на блок управления, который он просматривает для проверки/увеличения/etc. атомных счетчиков, чтобы определить, истек ли объект, или посмотреть, может ли он получить блокировку. Если вы хотите реализовать внутреннюю оптимизацию для std::weak_ptr, вам нужно как-то проверить состояние блока управления, а затем атомарно удалить указатель на блок управления, если указатель истек. Это может вызвать накладные расходы (даже если это может быть сделано просто с атоматикой, все равно будет иметь накладные расходы) при каждом доступе к std::weak_ptr, все ради небольшой оптимизации.

Ответ 2

  • Первая версия GetSelection лучше для подавляющего большинства случаев. Эта версия может быть const и не нуждается в дополнительном коде синхронизации, чтобы быть потокобезопасной.

  • В общем коде библиотеки, где точное использование шаблона не может быть предсказано заранее, первая версия по-прежнему предпочтительна. Однако в ситуации, когда код синхронизации уже существует, защищая доступ к weak_ptr, не может повредить вызов в reset для освобождения памяти и более быстрое использование указателя быстрее. Эта очень небольшая оптимизация сама по себе не стоит вставлять в этот код синхронизации.

  • Учитывая первые два ответа, этот последний вопрос спорный. Однако здесь есть два обоснованных аргумента в пользу того, что weak_ptr::lock автоматически reset указатель, если он истек, истекает:

    • При таком поведении невозможно реализовать weak_ptr::owner_before и, таким образом, использовать weak_ptr как тип ключа в ассоциативном контейнере.

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

Альтернативное решение:
Если потерянная память считается реальной проблемой, которая должна быть решена (возможно, общие объекты действительно большие и/или целевая платформа имеет очень ограниченную память), другой вариант заключается в создании общих объектов с помощью shared_ptr<T>(new T) вместо make_shared<T>. Это освободит память, выделенную для T еще раньше (когда последний shared_ptr указывает на ее уничтожение), в то время как маленький блок управления живет отдельно.