Рассмотрим следующую реализацию односвязного списка:
struct node {
std::unique_ptr<node> next;
ComplicatedDestructorClass data;
}
Теперь предположим, что я перестал использовать некоторый std::unique_ptr<node> head
экземпляр, который затем выходит из области видимости, вызывая вызов его деструктора.
Будет ли это удар по моему стеку для достаточно больших списков? Справедливо ли предположить, что компилятор будет выполнять довольно сложную оптимизацию (встроенный unique_ptr
деструктор в node
, а затем использовать хвостовую рекурсию), что будет намного сложнее, если я сделаю следующее (поскольку data
деструктор будет запутывать next
, делая его трудным чтобы компилятор заметил потенциальную возможность переупорядочения и возможности хвостового вызова):
struct node {
std::shared_ptr<node> next;
ComplicatedDestructorClass data;
}
Если data
каким-то образом есть указатель на его, node
тогда это может быть даже невозможно для хвостовой рекурсии (хотя, конечно, мы должны стремиться избегать таких нарушений инкапсуляции).
В общем, как можно иначе уничтожить этот список? Мы не можем просмотреть список и удалить «текущий» узел, потому что у общего указателя нет release
! Единственный способ - с помощью пользовательского удалителя, который действительно воняет для меня.
gcc -O3
не удалось оптимизировать хвостовую рекурсию (в сложном примере).Ответы:
Да, это в конечном итоге
node
разрушит ваш стек, если только компилятор не применяет оптимизацию хвостового вызова к деструктору иshared_ptr
деструктору. Последнее чрезвычайно зависит от стандартной реализации библиотеки. Например, Microsoft STL никогда этого не сделает, потому чтоshared_ptr
сначала уменьшает счетчик ссылок своего pointee (возможно, уничтожая объект), а затем уменьшает счетчик ссылок своего блока управления (слабый счетчик ссылок). Таким образом, внутренний деструктор не является хвостовым вызовом. Это также виртуальный вызов , который снижает вероятность его оптимизации.Типичные списки решают эту проблему, не имея один узел, владеющий другим, а имея один контейнер, который владеет всеми узлами, и использует цикл для удаления всего в деструкторе.
источник
shared_ptr
в конце. Я не могу полностью избавиться от указателей, так как мне нужна была безопасность потоков.std::atomic_*
перегрузок для них, нет?std::atomic<node*>
и дешевле.Поздний ответ, но так как никто не предоставил его ... Я столкнулся с той же проблемой и решил ее, используя собственный деструктор:
Если у вас действительно есть список , т. Е. Каждому узлу предшествует один узел, и у него самое большее один подписчик, а ваш
list
указатель на первыйnode
, то вышеприведенное должно работать.Если у вас есть нечеткая структура (например, ациклический граф), вы можете использовать следующее:
Идея в том, что когда вы делаете:
Старый общий указатель
next
уничтожен (потому чтоuse_count
он сейчас0
), и вы указываете на следующее. Это делает то же самое, что деструктор по умолчанию, за исключением того, что он делает это итеративно, а не рекурсивно и, таким образом, избегает переполнения стека.источник
next = std::move(next->next)
вызовомnext->~node()
.next->next
становится недействительным (оператором присваивания перемещения) доnext
уничтожения указанного значения , тем самым "останавливая" рекурсию. Я на самом деле использую этот код и эту работу (протестировано сg++
,clang
иmsvc
), но теперь, когда вы это говорите, я не уверен, что это определено стандартом (тот факт, что перемещенный указатель является недействительным до уничтожения старого объекта, указанного по указателю цели).operator=(std::shared_ptr&& r)
эквивалентноstd::shared_ptr(std::move(r)).swap(*this)
. Все еще из стандарта, конструктор перемещенияstd::shared_ptr(std::shared_ptr&& r)
делаетr
пустым, следовательноr
, пустым (r.get() == nullptr
) перед вызовомswap
. В моем случае это средствоnext->next
пусто до того, как старый объект, на который указывает указатель,next
будет уничтожен (приswap
вызове).f
включенnext
, нетnext->next
, и, посколькуnext->next
он нулевой, он немедленно останавливается.Честно говоря, я не знаком с алгоритмом освобождения смарт-указателей любого компилятора C ++, но я могу представить простой нерекурсивный алгоритм, который это делает. Учти это:
Следовательно, у стека не было бы шансов переполниться, и это намного проще, чем оптимизировать рекурсивный алгоритм.
Я не уверен, вписывается ли это в философию «умных указателей с почти нулевой стоимостью».
Я полагаю, что то, что вы описали, не приведет к переполнению стека, но вы можете попытаться создать умный эксперимент, чтобы доказать, что я неправ.
ОБНОВИТЬ
Ну, это доказывает неправильность того, что я написал ранее:
Эта программа вечно строит и деконструирует цепочку узлов. Это вызывает переполнение стека.
источник