Должен ли я вызвать сброс для weak_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.

Мои вопросы:

  1. Какая версия GetSelection будет лучшей практикой в ​​этой ситуации?

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

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


person slajoie    schedule 12.08.2014    source источник
comment
Через некоторое время я опубликую свой собственный (не совсем удовлетворительный) ответ, но мне бы очень хотелось посмотреть, что скажут другие.   -  person slajoie    schedule 13.08.2014
comment
Вы всегда можете сделать weak_ptr участника mutable во втором случае, если хотите, чтобы GetSelection остался const.   -  person T.C.    schedule 13.08.2014
comment
Я не совсем понимаю, что ты пишешь. Вы говорите, что после того, как selection.lock() вернул пустой std::shared_ptr, ранее управляемый объект каким-то образом не освобождается?   -  person Snps    schedule 13.08.2014
comment
@Т.С. Правильно, но тогда я бы сказал Раздражающе, эта версия требует, чтобы мой элемент выбора был изменчивым. Дело в том, что на самом деле я вообще не меняю наблюдаемое состояние объекта weak_ptr, я просто пытаюсь немного оптимизировать, что, возможно, можно было бы сделать внутри weak_ptr. Это напрямую связано с моим третьим вопросом (на который, я думаю, я знаю ответ, но я надеюсь на более информированные мнения, чем мое).   -  person slajoie    schedule 13.08.2014
comment
@Snps Я говорю, что память для него не освобождается; хотя он хорошо разрушен. Это связано с оптимизацией в make_shared. См. раздел примечаний на [en.cppreference.com/w/cpp/memory. /shared_ptr/make_shared].   -  person slajoie    schedule 13.08.2014
comment
@slajoie Хорошо, я раньше этого не понимал.   -  person Snps    schedule 13.08.2014
comment
Информация, связанная с данной: Влияет ли std::weak_ptrs на освобождение памяти, выделенной std::make_shared? и Как работает weak_ptr?   -  person Snps    schedule 13.08.2014
comment
@Snps Спасибо за ссылки. Я думал, что прочитал все вопросы по этой теме, но, похоже, я слишком полагался на теги.   -  person slajoie    schedule 13.08.2014


Ответы (2)


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

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

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

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

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

person Robert Allan Hennigan Leahy    schedule 12.08.2014
comment
Я включил код моей текущей ситуации, потому что это заставило меня задуматься об этом и попытаться сделать вопрос максимально ясным. Я согласен с вашим ответом на мой первый вопрос (и я думаю, что он останется в силе, даже когда я реализую множественный выбор и потенциально буду иметь тысячи этих потраченных впустую блоков), но я думаю, что другие 2 вопроса намного интереснее, и именно здесь я не согласен (см. дальнейшие комментарии). - person slajoie; 13.08.2014
comment
Ваш ответ на мой второй вопрос на самом деле ничего не добавляет к тому, что уже было в вопросе. Моя точка зрения заключалась в том, что если я пишу код шаблона, который не имеет ни малейшего представления о проблемной области или о том, где могут быть узкие места при создании экземпляра шаблона, не должен ли я ошибаться в сторону благоразумия, даже если это означает, что нужно написать немного больше? сложный код? В частности, случай make_shared<T[]> кажется мне достаточно убедительным. - person slajoie; 13.08.2014
comment
Что касается третьего ответа: да, lock и expired должны быть потокобезопасными. Однако полностью пустой weak_ptr быстрее заблокировать (или проверить на истечение срока действия или уничтожить), чем тот, который все еще указывает на блок управления, поэтому я не вижу, где вы получаете дополнительные накладные расходы (на самом деле в целом это было бы быстрее). Первый вызов lock после уничтожения общего объекта оплатит стоимость ~weak_ptr авансом, но после этого каждый вызов будет менее затратным (включая уничтожение, когда вы вернете всю стоимость). Это дополнительная оптимизация в дополнение к ранее освобожденному блоку памяти. - person slajoie; 13.08.2014
comment
@slajoie: При написании шаблона, который делает то, о чем вы спрашиваете, вызов reset не будет ошибкой с точки зрения благоразумия. Вы будете писать код с дополнительной ветвью (более медленной), которая больше не является const (т.е. может вызываться из меньшего количества контекстов), и которая отказывается от потокобезопасности, ради чего? Возможность того, что в каком-то очень конкретном и маловероятном сценарии вы не столкнетесь с узким местом использования памяти? Полезность константности и безопасности потоков будет гораздо важнее для универсальной реализации, чем активное освобождение в подавляющем большинстве случаев. - person Robert Allan Hennigan Leahy; 13.08.2014
comment
@slajoie: Похоже, вы упускаете из виду тот факт, что накладные расходы на реализацию механизма, который вы предлагаете в lock и expired, не будут оплачиваться только тогда, когда вы вызываете lock или expired, а срок действия указателя фактически истек. Плата будет выплачиваться каждый раз, когда вы взаимодействуете с std::weak_ptr, независимо от того, истек ли срок действия базового указателя или нет. - person Robert Allan Hennigan Leahy; 13.08.2014
comment
Я не собирался отказываться от потокобезопасности или константности. Моя проблема в том, что я думал, что эту идею можно реализовать внутри weak_ptr без дополнительной синхронизации, потому что там уже есть что-то, что он должен использовать для выполнения своей текущей работы, так почему бы не сделать еще одну вещь один раз за weak_ptr время жизни? Но теперь я понимаю, что вы все это время имели в виду: существующий потокобезопасный код позволяет только запись в управляющий блок, но он не покрывает указатель на него. Спасибо за объяснение (дважды)! Я все равно опубликовал свой (измененный) ответ, потому что в нем есть некоторые дополнения, которые люди могут найти полезными. - person slajoie; 13.08.2014

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

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

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

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

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

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

person slajoie    schedule 13.08.2014