Должен ли я называть 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
указывает на ее уничтожение), в то время как маленький блок управления живет отдельно.