Чи знищить великий список переповнення моєї стеки?


12

Розглянемо таку реалізацію списку, що стосується окремо:

struct node {
    std::unique_ptr<node> next;
    ComplicatedDestructorClass data;
}

Тепер, припустимо, я припиняю використовувати якийсь std::unique_ptr<node> headекземпляр, який потім виходить за межі, викликаючи виклик його деструктора.

Чи вдасться це підірвати мій стек для достатньо великих списків? Чи справедливо припустити, що компілятор зробить досить складну оптимізацію (вбудований unique_ptrдеструктор в node's, а потім використовувати хвостову рекурсію), що стає набагато складніше, якщо я виконую наступне (оскільки dataдеструктор заблукає next, робить це важким щоб компілятор помітив потенційну можливість упорядкування та виклику хвоста):

struct node {
    std::shared_ptr<node> next;
    ComplicatedDestructorClass data;
}

Якщо dataякимось чином є вказівник на його, nodeто для рецидиву хвоста це може бути навіть неможливим (хоча, звичайно, ми повинні прагнути уникати таких порушень інкапсуляції).

Загалом, як тоді знищити цей список? Ми не можемо пройти через список та видалити "поточний" вузол, оскільки спільний вказівник не має release! Єдиний спосіб - із користувацьким делетером, який мені справді смердючий.


1
Що варто, навіть без порушення інкапсуляції, згаданого у другому випадку, gcc -O3не вдалося оптимізувати хвостову рекурсію (на складному прикладі).
VF1

1
Там ви маєте свою відповідь: Це може підірвати ваш стек, якщо компілятор не зможе оптимізувати рекурсію.
Барт ван Іґен Шенау

@BartvanIngenSchenau Я думаю, це ще один приклад цієї проблеми . Це теж справжній сором, оскільки мені подобається розумна чистота вказівника.
VF1

Відповіді:


7

Так, це врешті-решт підіб'є ваш стек, якщо тільки компілятор не застосує оптимізацію хвостових викликів до nodedestructor і shared_ptr s destructor. Останнє вкрай залежить від стандартної реалізації бібліотеки. Наприклад, STL Майкрософт ніколи цього не зробить, оскільки shared_ptrспочатку декрементує опорний підрахунок свого керівника (можливо, знищує об'єкт), а потім зменшує контрольний номер контрольного блоку (слабкий відліку). Тож внутрішній деструктор - це не хвостик. Це також віртуальний дзвінок , що робить ще меншою ймовірність його оптимізації.

Типові списки вирішують цю проблему, не маючи одного вузла наступним, а маючи один контейнер, який володіє всіма вузлами, і використовує цикл, щоб видалити все в деструкторі.


Так, я реалізував "типовий" алгоритм видалення списку зі спеціальним делетером для тих, хто shared_ptrзрештою. Я не можу повністю позбутися покажчиків, оскільки мені була потрібна безпека нитки.
VF1

Я не знав , що загальний покажчик «лічильник» об'єкт повинен мати віртуальний деструктор або, я завжди вважав , що це просто POD тримає сильні реф + слабкі рефи + Deleter ...
VF1

@ VF1 Ви впевнені, що покажчики надають вам безпеку потоку, яку ви хочете?
Себастьян Редл

Так - у цьому вся суть std::atomic_*перевантажень для них, ні?
VF1

Так, але це нічого, чого ви std::atomic<node*>теж не можете досягти , і дешевше.
Себастьян Редл

5

Пізня відповідь, але оскільки ніхто її не надав ... Я зіткнувся з тією ж проблемою і вирішив її за допомогою спеціального деструктора:

virtual ~node () throw () {
    while (next) {
        next = std::move(next->next);
    }
}

Якщо у вас дійсно є список , тобто кожному вузлу передує один вузол і має максимум одного послідовника, а ваш list- вказівник на перший node, вищезгадане має працювати.

Якщо у вас є якась нечітка структура (наприклад, ациклічний графік), ви можете використовувати наступне:

virtual ~node () throw () {
    while (next && next.use_count() < 2) {
        next = std::move(next->next);
    }
}

Ідея полягає в тому, що коли ви робите:

next = std::move(next->next);

Старий загальний покажчик nextзнищений (тому що його use_countзараз є 0), і ви вказуєте на наступне. Це робить точно так само, як деструктор за замовчуванням, за винятком того, що він робить це ітеративно замість рекурсивно і, таким чином, уникає переповнення стека.


Цікава ідея. Не впевнений, що відповідає вимогам ОП щодо безпеки різьблення, але, безумовно, хороший спосіб вирішити цю проблему в інших аспектах.
Жуль

Якщо ви не перевантажили оператора переміщення, я не впевнений, яким чином цей підхід насправді нічого економить - у реальному списку кожна умова в той час як буде оцінена щонайбільше відразу, з next = std::move(next->next)викликом next->~node()рекурсивно.
VF1

1
@ VF1 Це працює тому, що next->nextвін недійсний (оператором присвоєння переміщення) до nextзнищення значення, вказаного на , тим самим "зупиняючи" рекурсію. Я фактично використовую цей код і цю роботу (перевірену g++, clangі msvc), але тепер, коли ви це скажете, я не впевнений, що це визначено стандартом (той факт, що переміщений покажчик недійсний перед знищенням старого об'єкта вказував за цільовим вказівником).
Холт

@ VF1 Update: Відповідно до стандарту, 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викликом).
Холт

1
@ VF1 Ваш код не той самий - виклик до fввімкнено next, ні next->next, і оскільки next->nextце недійсне, він негайно припиняється.
Холт

1

Якщо чесно, я не знайомий з алгоритмом розбиття інтелектуальних покажчиків будь-якого компілятора C ++, але я можу уявити простий, нерекурсивний алгоритм, який це робить. Врахуйте це:

  • У вас черга розумних покажчиків чекає на розселення.
  • У вас є функція, яка приймає перший вказівник і розміщує його, і повторює це, доки черга не буде порожньою.
  • Якщо розумний вказівник потребує розстановки, його заштовхують у чергу, і вищевказана функція викликається.

Отже, не буде шансу стек переповнюватись, і набагато простіше оптимізувати рекурсивний алгоритм.

Я не впевнений, чи вписується це у філософію "розумних покажчиків майже з нульовою вартістю".

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

ОНОВЛЕННЯ

Ну, це доводить неправильність того, що я писав раніше:

#include <iostream>
#include <memory>

using namespace std;

class Node;

Node *last;
long i;

class Node
{
public:
   unique_ptr<Node> next;
   ~Node()
   {
     last->next.reset(new Node);
     last = last->next.get();
     cout << i++ << endl;
   }
};

void ignite()
{
    Node n;
    n.next.reset(new Node);
    last = n.next.get();
}

int main()
{
    i = 0;
    ignite();
    return 0;
}

Ця програма вічно будує і деконструює ланцюг вузлів. Це викликає переповнення стека.


1
Ах, ти маєш на увазі використовувати стиль продовження проходження? Ефективно саме це ви описуєте. Однак я швидше принесу в жертву розумні покажчики, ніж будувати інший список на купі лише для того, щоб перенести старий.
VF1

Я помилявся. Я відповідно змінив свою відповідь.
Gábor Angyal
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.