Серебряной пули нет ... и shared_ptr
определенно не одна.
Мой первый вопрос: вам нужны все эти общие указатели?
Лучший способ избежать циклических ссылок - определить политику жизни каждого объекта и убедиться, что они совместимы.Это может быть легко задокументировано:
- вы передаете мне ссылку, я ожидаю, что объект будет жить в течение вызова функции, но не более
- вы передаете мне
unique_ptr
, ятеперь я отвечаю за объект - вы передаете мне
shared_ptr
, я рассчитываю, что смогу самостоятельно держать объект в руках, не оказывая негативного влияния на вас
СейчасЕсть редкие ситуации, когда использование shared_ptr
действительно необходимо.Обозначение кэшей заставляет меня думать, что это может быть ваш случай, по крайней мере, для некоторых применений.
В этом случае вы можете (по крайней мере неофициально) применять многоуровневый подход.
- Определить количество слоев, от
0
(базовое) до бесконечного - Каждый тип объекта приписывается слою, несколько типов могут совместно использоватьтот же слой
- Объект типа
A
может содержать shared_ptr
только для объекта типа B
только в том случае, если Layer(A) > Layer(B)
Примечание.что мы прямо запрещаем родственные отношения.При такой схеме невозможно сформировать круг ссылок.Действительно, мы получаем DAG (направленный ациклический граф).
Теперь, когда тип создается, ему должен быть присвоен номер слоя, и это должно быть задокументировано (предпочтительно в коде).
Объект может изменить слоя, однако:
- , если номер его слоя уменьшается, то вы должны пересмотреть ссылки, которые он содержит (легко)
- , еслиномер его слоя увеличивается, тогда вы должны пересмотреть все ссылки на него (жесткие)
Примечание: условно, типы объектов, которые не могут содержать никаких ссылок, обычно находятся в слое 0
.
Примечание 2: Сначала я наткнулся на это соглашение в статье Херба Саттера, где он применил его к мьютексам и попытался предотвратить тупик.Это адаптация к текущей проблеме.
Это может быть применено немного более автоматически (компилятором), если вы готовы работать с существующей кодовой базой.
Мы создаем новый класс SharedPtr
, осведомленный о нашей схеме размещения:
template <typename T>
constexpr unsigned getLayer(T const&) { return T::Layer; }
template <typename T, unsigned L>
class SharedPtrImpl {
public:
explicit SharedPtrImpl(T* t): _p(t)
{
static_assert(L > getLayer(std::declval<T>()), "Layering Violation");
}
T* get() const { return _p.get(); }
T& operator*() const { return *this->get(); }
T* operator->() const { return this->get(); }
private:
std::shared_ptr<T> _p;
};
Каждому типу, который может содержаться в таком SharedPtr
, присваивается свой слой статически, и мы используем базовый класс, чтобы помочь намout:
template <unsigned L>
struct LayerMember {
static unsigned const Layer = L;
template <typename T>
using SharedPtr<T> = SharedPtrImpl<T, L>;
};
И теперь мы можем легко использовать его:
class Foo: public LayerMember<3> {
public:
private:
SharedPtr<Bar> _bar; // statically checked!
};
Однако этот подход к кодированию немного более сложен, я думаю, что соглашение вполне может быть достаточным;)