Weak_ptr VS shared_ptr в графе node родительский список

У меня есть ориентированный ациклический граф, реализуемый классами Graph и Node. Каждый Node имеет список указателей на childern и список указателей на родителей. Недавно я добавил родителей, потому что некоторые алгоритмы требовали быстрого доступа к родительскому списку, а график небольшой, всего несколько соединений на node, поэтому проблем с памятью не возникает.

В списке Child используется std:: shared_ptr, так что узлы хранятся в памяти, по крайней мере, до тех пор, пока у них есть родители. Но я не хочу, чтобы Node владел родителями, поэтому я использовал weak_ptr для указателей на родителей.

Но тогда возникла проблема с алгоритмами. Алгоритм должен создать новый shared_ptr из weak_ptr, поэтому я не могу напрямую использовать operator ==, а использование стандартных функций, таких как std:: find(), требует записи лямбда-функции, которая называется my_weak_ptr.lock(), а затем сравнивает ее к некоторому shared_ptr.

Если я переключусь на shared_ptr, любая небольшая ошибка в коде, который может быть использован для удаления Node, может привести к утечке памяти. Или, если у меня есть указатель на Node, который уже удален, код сможет получить доступ к Node, который не должен существовать, поэтому найти некоторые ошибки могут значительно сложнее. Но работа с shared_ptr столь же безопасна, как и weak_ptr с точки зрения не разыменования/удаления/etc. когда не предполагается, (так что это лучше, чем исходный указатель на С++) и std:: find() можно использовать напрямую, поскольку shared_ptr может быть разыменован, в отличие от weak_ptr.

Существует ли здесь "лучший" дизайн, или это проблема этой конкретной ситуации, в зависимости от, например, сколько имеет значение, если я выполняю дополнительную операцию weak_ptr:: lock() или рискую найти труднодоступные ошибки?

Ответ 1

Как вы сказали сами, использование shared_ptr в обоих направлениях создаст круги, которые создают утечки памяти и их трудно найти и сломать - вы потеряете (почти) все преимущества, предоставляемые shared_ptr. Итак, weak_ptr должно быть.

Вы говорите, что ваши алгоритмы должны блокировать weak_ptr - я прошу разницы. Алгоритмы должны получить родительский shared_ptr из node. Задача node заблокировать родительский weak_ptr и вернуть результат, либо правильно установить родительский node, либо NULL.

Это деталь реализации, где узлы хранят своих родителей как shared_ptr или weak_ptr. Инкапсулируйте эту информацию, предоставив shared_ptr любым клиентам.

class Node
{
  /* ... */
  std::weak_ptr<Node> parent;
public:
  std::shared_ptr<Node> getParent()
  {
    return parent.lock();
  }
};

Edit: Конечно, концептуально то же самое имеет место, если имеется более одного родителя.

Edit2: В комментариях вы упомянули алгоритмы, повторяющие список ваших родителей, что делает необходимым писать lambdas для каждого алгоритма. Если вы часто используете эти алгоритмы, подумайте о том, чтобы написать адаптер итератора, который автоматически блокирует цель weak_ptr и возвращает shared_ptr:

template <class WPIterator>
struct LockTheWeakIterator
{
  //static_assert that WPiterator value_type is some weak_ptr
  //typedef all those iterator typedefs
  typedef typename WPIterator::value_type::element_type element_type;

  shared_ptr<element_type> operator*()
  { return iter->lock(); }

  //provide all the other operators - boost.operators might help with that...

  WPIterator iter;
};

template <class IT>
LockTheWeakIterator<It> lockTheWeak(It iter);


//somewhere...
auto theParentIter = std::find_if(lockTheWeak(parents.begin()), 
  lockTheWeak(parents.end()), 
  whatIAmLookingFor);

Ответ 2

Большинство ориентированных ациклических графов вообще не нуждаются в слабых указателях на своих родителей, но работают с простыми указателями. В обоих случаях каждая ответственность node должна удаляться из каждого родительского списка клиентов после их удаления. Если вам нужно получить общий указатель с какого-то родительского указателя в какой-то особой ситуации, вы можете использовать std:: shared_from_this так же, как вы бы сейчас использовали lock(). Таким образом, вы сохраняете работу для создания и обработки общих указателей повсюду, но только там, где они вам нужны.