Реализация суицидальных объектов с использованием `std:: weak_ptr`
Я рассматриваю возможность использования "объектов самоубийства" для моделирования объектов в игре, то есть объектов, способных удалять себя. Теперь обычная реализация С++ 03 (простой старый delete this
) ничего не делает для других объектов, которые потенциально ссылаются на объект самоубийства, поэтому я использую std::shared_ptr
и std::weak_ptr
.
Теперь для дампа кода:
#include <memory>
#include <iostream>
#include <cassert>
struct SuObj {
SuObj() { std::cout << __func__ << '\n'; }
~SuObj() { std::cout << __func__ << '\n'; }
void die() {
ptr.reset();
}
static std::weak_ptr<SuObj> create() {
std::shared_ptr<SuObj> obj = std::make_shared<SuObj>();
return (obj->ptr = std::move(obj));
}
private:
std::shared_ptr<SuObj> ptr;
};
int main() {
std::weak_ptr<SuObj> obj = SuObj::create();
assert(!obj.expired());
std::cout << "Still alive\n";
obj.lock()->die();
assert(obj.expired());
std::cout << "Deleted\n";
return 0;
}
Вопрос
Этот код работает нормально. Тем не менее, я бы хотел, чтобы кто-то еще посмотрел на него. Имеет ли смысл этот код? Я слепо плавал в землях? Должен ли я отказаться от клавиатуры и начать заниматься искусством прямо сейчас?
Надеюсь, этот вопрос достаточно сузился для SO. Считается немного крошечным и низкоуровневым для CR.
Незначительная точность
Я не намерен использовать это в многопоточном коде. Если возникнет такая необходимость, я обязательно передумаю все это.
Ответы
Ответ 1
Когда у вас есть время жизни объекта на основе shared_ptr
, время жизни вашего объекта - это "время жизни" объединения shared_ptr
, владеющего им коллективно.
В вашем случае у вас есть внутренний shared_ptr
, и ваш объект не умрет, пока не истечет срок выполнения внутренней shared_ptr
.
Однако это не означает, что вы можете совершить самоубийство. Если вы удалите эту последнюю ссылку, ваш объект будет продолжать существовать , если у кого есть .lock()
'd weak_ptr
и сохранен результат. Поскольку это единственный способ получить доступ к объекту извне, может случиться 1.
Короче говоря, die()
может не убить объект. Его можно было бы назвать remove_life_support()
, поскольку что-то еще могло сохранить объект в живых после удаления поддержки жизни.
Кроме этого, ваш дизайн работает.
1
Вы могли бы сказать "хорошо, тогда вызывающие должны просто не поддерживать shared_ptr
вокруг", но это не работает, поскольку проверка того, что объект действителен, действителен только в том случае, если сохраняется shared_ptr
. Кроме того, раскрывая способ создания shared_ptr
, у вас нет гарантий типа, что клиентский код не будет хранить их (случайно или по назначению).
Модель, основанная на транзакциях (где вы проходите лямбда внутри, и она работает внутри нее) может помочь с этим, если вы хотите серьезно параноидальную устойчивость.
Или вы можете жить с объектом, который иногда живет слишком долго.
Рассмотрите возможность скрытия этих беспорядочных деталей за регулярным типом (или почти регулярным), который имеет pImpl
для неприятной проблемы управления памятью. Это pImpl
может быть weak_ptr
с указанной семантикой.
Затем пользователям вашего кода необходимо взаимодействовать только с обычной (или псевдорегулярной) оболочкой.
Если вы не хотите, чтобы клонирование было легким, отключите построение/назначение копии и выведите только перемещение.
Теперь ваше неприятное управление памятью скрывается за лабиринтом, и если вы решите, что все это неправильно, внешний интерфейс pseudoRegular может иметь разные кишки.
Обычный тип в С++ 11
Ответ 2
Не прямой ответ, но потенциально полезная информация.
В хромовой кодовой базе есть концепция того, чего вы пытаетесь достичь. Они называют это WeakPtrFactory. Их решение не может быть непосредственно взято в ваш код, поскольку они имеют собственную реализацию, например. shared_ptr
и weak_ptr
, но дизайн будет полезен вам.
Я попытался его реализовать и выяснил, что проблему двойного удаления можно решить, перейдя во внутренний shared_ptr
пользовательский пустой дебетер - с этого момента ни один shared_ptrs, созданный из weak_ptr
не внутренний shared_ptr
, будет иметь возможность вызвать деструктор (снова) на свой объект.
Единственная проблема для решения - что, если ваш объект удален, а где-то еще вы сохраняете shared_ptr
? Но из того, что я вижу, это не может быть просто решена каким-либо магическим знанием и требует разработки всего проекта так, как это просто никогда не происходит, например. используя shared_ptr
только в локальной области и гарантируя, что некоторый набор операций (создание объекта самоубийства, используя его, заказывающий его самоубийство) может выполняться только в той же теме.
Ответ 3
Я понимаю, что вы пытаетесь создать минимальный пример для SO, но я вижу несколько проблем, которые вы захотите рассмотреть:
- У вас есть открытый конструктор и деструктор, поэтому технически там нет гарантии, что метод create() всегда используется.
- Вы можете создавать защищенные или частные, но это решение будет мешать использованию с std-алгоритмами и контейнерами.
- Это не гарантирует, что объект будет фактически разрушаться, поскольку до тех пор, пока кто-то имеет shared_ptr, он будет существовать. Это может быть или не быть проблемой для вашего варианта использования, но из-за этого я не думаю, что это добавит столько же значения, сколько вы заголовок.
- Это, скорее всего, будет запутанным и противоречивым для других разработчиков. Это может затруднить техническое обслуживание, даже если вы намерены сделать это проще. Это немного ценностное суждение, но я бы посоветовал вам рассмотреть, действительно ли это проще управлять.
Я рекомендую вам задуматься над управлением памятью. Дисциплинированное использование shared_ptr и weak_ptr поможет с проблемами управления памятью - я бы советовал не пытаться, чтобы экземпляр попытался управлять собственным жизненным циклом.
Что касается художественных исследований... Я бы рекомендовал только, если это действительно ваша страсть! Удачи!