Подтвердить что ты не робот

Как определить циклы при использовании shared_ptr

shared_ptr - это интеллектуальный указатель подсчета ссылок в библиотеке Boost.

Проблема с подсчетом ссылок заключается в том, что он не может избавиться от циклов. Мне интересно, как можно решить это на С++.

Пожалуйста, никаких предложений вроде: "не делать циклы" или "использовать слабые_ptr".

Edit

Мне не нравятся предложения, которые говорят, что просто используйте weak_ptr, потому что, очевидно, если вы знаете, что создадите цикл, тогда у вас не будет проблемы. Вы также не можете знать, что во время компиляции у вас будет цикл, если вы создадите shared_ptrs во время выполнения.

Так что, пожалуйста, удалите ответы, в которых используется weak_ptr, потому что я специально попросил не иметь таких ответов...

4b9b3361

Ответ 1

Я не нашел гораздо лучшего метода, чем рисовать большие графики UML и смотреть на циклы.

Чтобы отлаживать, я использую счетчик экземпляра, идущий в реестр, например:

template <DWORD id>
class CDbgInstCount
{
public:
#ifdef _DEBUG
   CDbgInstCount()   { reghelper.Add(id, 1); }
   CDbgInstCount(CDbgInstCount const &) {  reghelper.Add(id, 1); }
   ~CDbgInstCount()  { reghelper.Add(id, -1); }
#else
#endif
};

Мне просто нужно добавить это к рассматриваемым классам и посмотреть на реестр.

(Идентификатор, если он указан, например, "XYZ!", будет преобразован в строку. К сожалению, вы не можете указать строчную константу в качестве параметра шаблона)

Ответ 2

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

Я не понимаю, почему предложение weak_ptr считается бесполезным.

Ответ 3

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

Спросите, как именно вы определяете циклические ссылки. Правда в том, что в сложном проекте некоторые ссылочные циклы косвенны и трудно распознаются.

Ответ заключается в том, что вы не должны делать ложные объявления, которые оставляют вас уязвимыми для циклических ссылок. Я серьезно, и я критикую очень популярную практику - слепо используя shared_ptr для всего.

В вашем дизайне должно быть ясно, какие указатели являются владельцами и которые являются наблюдателями.

Для владельцев использовать shared_ptr

Для наблюдателей используйте weak_ptr - все они, а не только те, которые, по вашему мнению, могут быть частью цикла.

Если вы будете следовать этой практике, циклические ссылки не вызовут никаких проблем, и вам не нужно будет беспокоиться о них. Конечно, у вас будет много кода для записи, чтобы преобразовать все эти слабые_ptrs в shared_ptrs, когда вы хотите их использовать. Boost действительно не соответствует заданию.

Ответ 4

Довольно легко обнаружить циклы:

  • установить количество до большого количества, скажем 1000 (точный размер зависит от вашего приложения)
  • начните с интересующего вас пользователя pionter и следуйте указателям от него.
  • для каждого указателя, который вы используете, уменьшите счетчик
  • если счетчик падает до нуля, прежде чем вы достигнете конца цепочки указателей, у вас есть цикл

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

Ответ 8

Вам, вероятно, нужна техника сбора мусора, такая как Mark and Sweep. Идея этого алгоритма:

  • Сохраните список со ссылками на все выделенные блоки памяти.
  • В какой-то момент вы начинаете сборщик мусора:
    • Он сначала маркирует все блоки, которые он может получить, без использования списка ссылок.
    • Он проходит через список, стирающий каждый элемент, который не может быть помечен, подразумевая, что он больше недоступен, поэтому он не является полезным.

Поскольку вы используете shared_ptr, все существующие существующие указатели, которых вы не можете достичь, должны рассматриваться как члены цикла.

Реализация

Ниже я описываю очень наивный пример того, как реализовать часть sweep() алгоритма, но он будет reset() всех остальных указателей на коллекторе.

В этом коде хранятся указатели shared_ptr<Cycle_t>. Класс Collector отвечает за отслеживание всех указателей и их удаление при выполнении sweep().

#include <vector>
#include <memory>

class Cycle_t;
typedef std::shared_ptr<Cycle_t> Ref_t;

// struct Cycle;
struct Cycle_t {
  Ref_t cycle;

  Cycle_t() {}
  Cycle_t(Ref_t cycle) : cycle(cycle) {}
};

struct collector {
  // Note this vector will grow endlessy.
  // You should find a way to reuse old links
  std::vector<std::weak_ptr<Cycle_t>> memory;

  // Allocate a shared pointer keeping
  // a weak ref on the memory vector:
  inline Ref_t add(Ref_t ref) {
    memory.emplace_back(ref);
    return ref;
  }
  inline Ref_t add(Cycle_t value) {
    Ref_t ref = std::make_shared<Cycle_t>(value);
    return add(ref);
  }
  inline Ref_t add() {
    Ref_t ref = std::make_shared<Cycle_t>();
    return add(ref);
  }

  void sweep() {
    // Run a sweep algorithm:
    for (auto& ref : memory) {
      // If the original shared_ptr still exists:
      if (auto ptr = ref.lock()) {
        // Reset each pointer contained within it:
        ptr->cycle.reset();

        // Doing this will trigger a deallocation cascade, since
        // the pointer it used to reference will now lose its
        // last reference and be deleted by the reference counting
        // system.
        //
        // The `ptr` pointer will not be deletd on the cascade
        // because we still have at least the current reference
        // to it.
      }
      // When we leave the loop `ptr` loses its last reference
      // and should be deleted.
    }
  }
};

Затем вы можете использовать его следующим образом:

Collector collector;

int main() {
  // Build your shared pointers:
  {
    // Allocate them using the collector:
    Ref_t c1 = collector.add();
    Ref_t c2 = collector.add(c1);

    // Then create the cycle:
    c1.get()->cycle = c2;

    // A normal block with no cycles:
    Ref_t c3 = collector.add();
  }

  // In another scope:
  {
    // Note: if you run sweep an you still have an existing
    // reference to one of the pointers in the collector
    // you will lose it since it will be reset().
    collector.sweep();
  }
}

Я тестировал его с помощью Valgrind, и не было никаких утечек памяти или блоков "все еще достижимых", поэтому он, вероятно, работает как ожидалось.

Некоторые примечания по этой реализации:

  • Вектор памяти будет расти бесконечно, вы должны использовать некоторую технику выделения памяти, чтобы убедиться, что она не занимает всю вашу рабочую память.
  • Можно утверждать, что нет необходимости использовать shared_ptr (который работает как GC подсчета ссылок) для реализации такого сборщика мусора, поскольку алгоритм Mark и Sweep уже выполнил бы работу.
  • Я не реализовал функцию mark(), потому что это осложнило бы этот пример, но это возможно, и я объясню это ниже.

Наконец, если вы обеспокоены (2), такого рода реализация не является неслыханной. CPython (основная реализация Python) использует смесь ссылок Count, Mark и Sweep, но главным образом для исторических причин.

Реализация функции mark():

Для реализации функции mark() вам необходимо внести некоторые изменения:

Требуется добавить атрибут bool marked; в Cycle_t и использовать его для проверки того, отмечен ли указатель или нет.

Вам нужно будет написать функцию Collector::mark(), которая будет выглядеть так:

void mark(Ref_t root) {
  root->marked = true;

  // For each other Ref_t stored on root:
  for (Ref_t& item : root) {
    mark(item);
  }
}

И тогда вы должны изменить функцию sweep(), чтобы удалить метку, если указатель отмечен, или reset() указатель:

void sweep() {
  // Run a sweep algorithm:
  for (auto& ref : memory) {
    // If it still exists:
    if (auto ptr = ref.lock()) {
      // And is marked:
      if (ptr->marked) {
        ptr->marked = false;
      } else {
        ptr->cycle.reset();
      }
    }
  }
}

Это было длинное объяснение, но я надеюсь, что это кому-то поможет.

Ответ 9

Ответ на старый вопрос, вы можете попробовать интрузивный указатель, который может помочь подсчитать, сколько раз передается ресурс.

#include <cstdlib>
#include <iostream>

#include <boost/intrusive_ptr.hpp>

class some_resource
{
    size_t m_counter;

public:
    some_resource(void) :
        m_counter(0)
    {
        std::cout << "Resource created" << std::endl;
    }

    ~some_resource(void)
    {
        std::cout << "Resource destroyed" << std::endl;
    }

    size_t refcnt(void)
    {
        return m_counter;
    }

    void ref(void)
    {
        m_counter++;
    }

    void unref(void)
    {
        m_counter--;
    }
};

void
intrusive_ptr_add_ref(some_resource* r)
{
    r->ref();
    std::cout << "Resource referenced: " << r->refcnt()
              << std::endl;
}

void
intrusive_ptr_release(some_resource* r)
{
    r->unref();
    std::cout << "Resource unreferenced: " << r->refcnt()
              << std::endl;
    if (r->refcnt() == 0)
        delete r;
}

int main(void)
{
    boost::intrusive_ptr<some_resource> r(new some_resource);
    boost::intrusive_ptr<some_resource> r2(r);

    std::cout << "Program exiting" << std::endl;

    return EXIT_SUCCESS;
}

Здесь результат возвращается.

Resource created 
Resource referenced: 1 
Resource referenced: 2 
Program exiting 
Resource unreferenced: 1
Resource unreferenced: 0 
Resource destroyed
*** Program Exit ***

Ответ 10

Я знаю, что вы сказали "no weak_ptr", но почему бы и нет? У вас будет голова со слабым_требом до хвоста, а хвост со слабой_ptr на голову предотвратит цикл.