Вложенное уничтожение shared_ptr вызывает переполнение стека

Это проблема дизайна (я знаю, почему это происходит, просто хочу посмотреть, как люди справляются с ней). Предположим, у меня есть простой связанный список struct:

struct List {
    int head;
    std::shared_ptr<List> tail;
};

shared_ptr позволяет обмениваться подсписками между несколькими списками. Однако, когда список становится очень длинным, в его деструкторе может произойти переполнение стека (вызванное рекурсивными выпусками shared_ptr s). Я попытался использовать явный стек, но это очень сложно, поскольку хвост может принадлежать нескольким спискам. Как я могу создать свой List, чтобы избежать этой проблемы?

ОБНОВЛЕНИЕ: Чтобы уточнить, я не изобретаю колесо (std::forward_list). List выше - это только упрощенная версия реальной структуры данных. Реальная структура данных представляет собой ориентированный ациклический граф, который, если вы думаете об этом, - это всего лишь много связанных списков с общими хвостами/головами. Обычно копировать граф обычно не стоит дорого, поэтому необходим обмен данными.

ОБНОВЛЕНИЕ 2: Я думаю о явном пересечении цепочки указателей и std::move, когда я иду. Что-то вроде:

~List()
{
    auto p = std::move(tail);
    while (p->tail != nullptr && p->tail.use_count() == 1) {
        // Some other thread may start pointing to `p->tail`
        // and increases its use count before the next line
        p = std::move(p->tail);
    }
}

Кажется, что это работает в одном потоке, но я беспокоюсь о безопасности потоков.

Ответ 1

Если у вас возникли проблемы с переполнением стека при уничтожении вашей связанной структуры данных, самым простым решением является простое выполнение отложенной очистки:

struct Graph {
    std::shared_ptr<Graph>  p1, p2, p3;   // some pointers in your datastructure
    static std::list<std::shared_ptr<Graph>>   deferred_cleanup;

    ~Graph() {
        deferred_cleanup.emplace_back(std::move(p1));
        deferred_cleanup.emplace_back(std::move(p2));
        deferred_cleanup.emplace_back(std::move(p3));
    }
    static void cleanup() {
        while (!deferred_cleanup.empty()) {
            std::list<std::shared_ptr<Graph>>  tmp;
            std::swap(tmp, deferred_cleanup);
            tmp.clear(); } }
};

и вам просто нужно периодически называть Graph::cleanup();.

Ответ 2

это должно это сделать. С небольшой работой он может быть легко выполнен в потоковом режиме (небольшая блокировка/атомизация в двигателе с отработавшим двигателем)

синопсис:

Разделение shared_ptr на узлы создаются с помощью специального деструктора, который вместо удаления node передает его на двигатель делетирования.

Реализация двигателя - одноэлементная. После уведомления о новом node, который нужно удалить, он добавляет node в очередь удаления. Если не удалено node, узлы в очереди удаляются по очереди (без рекурсии).

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

#include <memory>
#include <deque>
#include <stdexcept>
#include <iostream>

struct node;

struct delete_engine
{
    void queue_for_delete(std::unique_ptr<node> p);

    struct impl;
    static impl& get_impl();
};

struct node
{
    node(int d) : data(d) {}
    ~node() {
        std::cout << "deleting node " << data << std::endl;
    }

    static std::shared_ptr<node> create(int d) {
        return { new node(d),
            [](node* p) {
                auto eng = delete_engine();
                eng.queue_for_delete(std::unique_ptr<node>(p));
            }};
    }

    int data;
    std::shared_ptr<node> child;
};

struct delete_engine::impl
{
    bool _deleting { false };
    std::deque<std::unique_ptr<node>> _delete_list;

    void queue_for_delete(std::unique_ptr<node> p)
    {
        _delete_list.push_front(std::move(p));
        if (!_deleting)
        {
            _deleting = true;
            while(!_delete_list.empty())
            {
                _delete_list.pop_back();
            }
            _deleting = false;
        }
    }
};

auto delete_engine::get_impl() -> impl&
{
    static impl _{};
    return _;
}

void delete_engine::queue_for_delete(std::unique_ptr<node> p)
{
    get_impl().queue_for_delete(std::move(p));
}

struct tree
{
    std::shared_ptr<node> root;

    auto add_child(int data)
    {
        if (root) {
            throw std::logic_error("already have a root");
        }
        auto n = node::create(data);
        root = n;
        return n;

    }
};


int main()
{
    tree t;
    auto pc = t.add_child(6);
    pc = pc->child = node::create(7);

}

Ответ 3

std:: shared_ptr (и до этого boost:: shared_ptr) является и является стандартом де-факто для построения динамических систем с участием массивных DAG.

В действительности, группы DAG не получают такой глубокий (возможно, 10 или 12 алгоритмов в глубину вашего среднего сервера оценки FX?), поэтому рекурсивные удаления не являются проблемой.

Если вы думаете о создании огромной DAG с глубиной 10 000, тогда это может стать проблемой, но, честно говоря, я думаю, что это будет наименьшее из ваших беспокойств.

Повторяется аналогия с DAG как связанный список... на самом деле. Поскольку это ациклично, все ваши указатели, указывающие "вверх", должны быть shared_ptr, и все ваши обратные указатели (например, привязка подписных сообщений к алгоритмам раковины) должны быть слабыми_ptr, которые вы блокируете при запуске сообщения.

отказ от ответственности: я потратил много времени на разработку и построение информационных систем на основе ориентированных ациклических графиков параметризованных компонентов алгоритма с большим количеством общих компонентов (т.е. одного и того же алгоритма с одинаковыми параметрами).

Эффективность графика никогда не является проблемой. Узкие места:

  • изначально создавая график при запуске программы - там много шума в этой точке, но это происходит только один раз.

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