У чому полягає корисність `enable_shared_from_this`?


349

Я перебігав enable_shared_from_this, читаючи приклади Boost.Asio і після прочитання документації я все ще втрачаю, як це правильно використовувати. Чи може хтось, будь ласка, надати мені приклад та пояснення щодо використання цього класу має сенс.

Відповіді:


362

Це дозволяє вам отримати дійсний shared_ptrекземпляр this, коли все, що у вас є this. Без нього ви не мали б ніякого способу отримувати shared_ptrдо this, якщо ви вже мали один в якості члена. Цей приклад із документації для підвищення для enable_shared_from_this :

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_from_this();
    }
}

int main()
{
    shared_ptr<Y> p(new Y);
    shared_ptr<Y> q = p->f();
    assert(p == q);
    assert(!(p < q || q < p)); // p and q must share ownership
}

Метод f()повертає дійсний shared_ptr, навіть не маючи примірника члена. Зауважте, що ви не можете просто зробити це:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_ptr<Y>(this);
    }
}

Спільний покажчик, який повернувся, матиме інший підрахунок посилань від "належного", і один із них втратить і матиме звисаючу посилання, коли об'єкт буде видалений.

enable_shared_from_thisстав частиною стандарту C ++ 11. Ви також можете отримати його як звідти, так і з прискорення.


202
+1. Ключовим моментом є те, що "очевидна" техніка повернення shared_ptr <Y> (це) порушена, оскільки це завершується створенням декількох різних об'єктів shared_ptr з окремими підрахунками посилань. З цієї причини ви ніколи не повинні створювати більше одного shared_ptr з того самого необробленого покажчика .
j_random_hacker

3
Слід зазначити, що в C ++ 11 та пізніших версіях , абсолютно доцільно використовувати std::shared_ptrконструктор на сирому вказівнику, якщо він успадковує з std::enable_shared_from_this. Я не знаю, чи було оновлено семантику Boost, щоб це підтримати.
Метью

6
@MatthewHolder У вас є цитата на це? На cppreference.com я прочитав "Конструювання std::shared_ptrдля об'єкта, яким вже керує інший std::shared_ptr, не звертається до внутрішньо збережених слабких посилань і, таким чином, призведе до невизначеної поведінки". ( en.cppreference.com/w/cpp/memory/enable_shared_from_this )
Thorbjørn Lindeijer

5
Чому ти просто не можеш це зробити shared_ptr<Y> q = p?
Дан М.

2
@ ThorbjørnLindeijer, ви праві, це C ++ 17 і пізніше. Деякі реалізації дотримувались C ++ 16 семантики до її виходу. Слід використовувати правильне поводження для C ++ 11 до C ++ 14 std::make_shared<T>.
Метью

198

зі статті доктора Доббса про слабкі покажчики, я думаю, що цей приклад легше зрозуміти (джерело: http://drdobbs.com/cpp/184402026 ):

... такий код не працює належним чином:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

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

Аналогічно, якщо функції-члену потрібен shared_ptrоб'єкт, який належить об'єкту, до якого його викликають, він не може просто створити об'єкт на льоту:

struct S
{
  shared_ptr<S> dangerous()
  {
     return shared_ptr<S>(this);   // don't do this!
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->dangerous();
   return 0;
}

Цей код має ту саму проблему, що і в попередньому прикладі, хоча у більш тонкій формі. Коли він побудований, shared_ptr об'єкту sp1належить щойно виділений ресурс. Код всередині функції-члена S::dangerousне знає про цей shared_ptrоб'єкт, тому shared_ptrоб'єкт, від якого він повертається, відрізняється від sp1. Копіювання нового shared_ptrоб’єкта sp2не допомагає; коли sp2вийде за рамки, він вивільнить ресурс, а коли sp1вийде за межі, знову випустить ресурс.

Спосіб уникнути цієї проблеми - використовувати шаблон класу enable_shared_from_this. Шаблон приймає один аргумент типу шаблону, який є іменем класу, який визначає керований ресурс. Цей клас, у свою чергу, повинен бути публічно виведений із шаблону; подобається це:

struct S : enable_shared_from_this<S>
{
  shared_ptr<S> not_dangerous()
  {
    return shared_from_this();
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->not_dangerous();
   return 0;
}

Роблячи це, майте на увазі, що об’єкт, на який ви телефонуєте, shared_from_thisповинен належати shared_ptrоб'єкту. Це не спрацює:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}

15
Дякую, це ілюструє вирішення проблеми краще, ніж прийнята на даний момент відповідь.
Ґерценатор

2
+1: Гарна відповідь. Як осторонь замість shared_ptr<S> sp1(new S);цього можна віддати перевагу shared_ptr<S> sp1 = make_shared<S>();, див., Наприклад, stackoverflow.com/questions/18301511/…
Arun

4
Я впевнений, що останній рядок повинен бути прочитаний, shared_ptr<S> sp2 = p->not_dangerous();оскільки підводний камінь тут полягає в тому, що ви повинні створити shared_ptr звичайним способом, перш ніж shared_from_this()вперше дзвонити ! Це дійсно легко помилитися! До C ++ 17 це UB зателефонувати , shared_from_this()перш ніж точно один shared_ptr був створений звичайним способом: auto sptr = std::make_shared<S>();або shared_ptr<S> sptr(new S());. Вдячно від C ++ 17 далі буде кидати це.
AnorZaken


2
@AnorZaken Добре. Було б корисно, якби ви подали запит на редагування, щоб зробити це виправлення. Я щойно це зробив. Іншим корисним було б, щоб плакат не обирав суб'єктивні, контекстно-залежні назви методів!
підкреслюй_12

30

Ось моє пояснення з точки зору гайок і болтів (головна відповідь не «клацала» зі мною). * Зауважте, що це результат дослідження джерела для shared_ptr та enable_shared_from_this, який поставляється з Visual Studio 2012. Можливо, інші компілятори реалізують enable_shared_from_this по-різному ... *

enable_shared_from_this<T>додає приватний weak_ptr<T>екземпляр, до Tякого міститься " один справжній посилання " для екземпляра T.

Отже, коли ви вперше створюєте shared_ptr<T>на новій T *, внутрішній слабкий_ptr T * ініціалізується зі зворотним рахунком 1. Новий в shared_ptrосновному спирається на це weak_ptr.

Tможе, у своїх методах, викликати shared_from_thisотримання екземпляра shared_ptr<T>цього списку на той самий внутрішньо збережений контрольний номер . Таким чином, у вас завжди є одне місце, де T*зберігається повторний підрахунок, а не декілька shared_ptrекземплярів, які не знають один про одного, і кожен думає, що вони є тим, shared_ptrхто відповідає за підрахунок посилання Tта видалення його, коли їх посилання -кілька досягає нуля.


1
Це правильно, і насправді важлива частина полягає в So, when you first create...тому, що це вимога (як ви говорите, слабкий_ptr не ініціалізується, поки ви не передасте вказівник об'єктів у спільний_ptr ctor!), І ця вимога полягає в тому, де речі можуть жахливо помилитися, якщо ви не обережні. Якщо ви не створили no shared_ptr перед викликом, shared_from_thisви отримуєте UB - так само, якщо ви створите більше одного shared_ptr, ви також отримаєте UB. Ви повинні якось переконатися, що ви створили shared_ptr рівно один раз.
AnorZaken

2
Іншими словами, вся ідея enable_shared_from_thisкрихка для початку, оскільки справа полягає в тому, щоб мати змогу отримати shared_ptr<T>від a T*, але насправді, коли ви отримуєте вказівник, T* tяк правило, не безпечно припускати, що про нього вже поділяють чи ні, і зробити неправильну здогаду UB.
AnorZaken

" Внутрішній слабкий_ptr ініціалізується зі зворотним рахунком у 1 " слабкий ptr до T, не володіє розумним ptr до T. Слабкий ptr - це власний розумний перелік достатньої кількості інформації, щоб зробити власний ptr, який є "копією" іншого володіючого ptr. Слабкий ptr не має відліку. Він має доступ до кількості посилань, як і всі власники.
curiousguy

3

Зауважте, що використання boost :: intrusive_ptr не страждає від цієї проблеми. Це часто більш зручний спосіб подолати цю проблему.


Так, але enable_shared_from_thisдозволяє працювати з API, який спеціально приймає shared_ptr<>. На мою думку, такий API зазвичай робить Doing It Wrong (оскільки краще дозволити чомусь вище в стеку володіти пам'яттю), але якщо ви змушені працювати з таким API, це хороший варіант.
cdunn2001

2
Краще залишатись у межах норми наскільки можна.
Сергій

3

Це точно так само в c ++ 11 і пізніших версіях: це увімкнути можливість повернення thisяк спільний покажчик, оскільки thisдає вам необроблений покажчик.

інакше кажучи, це дозволяє вам перетворити такий код

class Node {
public:
    Node* getParent const() {
        if (m_parent) {
            return m_parent;
        } else {
            return this;
        }
    }

private:

    Node * m_parent = nullptr;
};           

в це:

class Node : std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getParent const() {
        std::shared_ptr<Node> parent = m_parent.lock();
        if (parent) {
            return parent;
        } else {
            return shared_from_this();
        }
    }

private:

    std::weak_ptr<Node> m_parent;
};           

Це буде працювати лише в тому випадку, якщо цими об'єктами завжди керує a shared_ptr. Ви можете змінити інтерфейс, щоб переконатися, що це так.
curiousguy

1
Ви абсолютно праві @curiousguy. Це само собою зрозуміло. Мені також подобається вводити всі свої спільні_ptr для поліпшення читабельності при визначенні моїх публічних API. Наприклад, замість цього std::shared_ptr<Node> getParent const()я, як правило, виставляю це як NodePtr getParent const()замість цього. Якщо вам абсолютно потрібен доступ до внутрішнього необробленого вказівника (найкращий приклад: робота з бібліотекою С), є std::shared_ptr<T>::getте, про що я ненавиджу, тому що я цей необроблений аксесуар вказівника використовував занадто багато разів з неправильної причини.
mchiasson

-3

Інший спосіб - додати weak_ptr<Y> m_stubчлена до class Y. Тоді пиши:

shared_ptr<Y> Y::f()
{
    return m_stub.lock();
}

Корисно, коли ви не можете змінити клас, з якого ви походите (наприклад, розширення бібліотеки інших людей). Не забувайте ініціалізувати елемент, наприклад m_stub = shared_ptr<Y>(this), його дія діє навіть під час конструктора.

Добре, якщо в ієрархії спадкування є більше таких заглушок, як це, це не завадить знищити об'єкт.

Редагувати: Як правильно вказав користувач nobar, код знищить об'єкт Y, коли призначення буде закінчено, а тимчасові змінні знищені. Тому моя відповідь неправильна.


4
Якщо ваш намір тут полягає в тому, щоб створити файл, shared_ptr<>який не видаляє його покажчик, це зайве. Ви можете просто сказати, return shared_ptr<Y>(this, no_op_deleter);де no_op_deleterзнаходиться об'єкт унарної функції, який Y*нічого не робить.
Джон Цвінк

2
Навряд чи здається, що це робоче рішення. m_stub = shared_ptr<Y>(this)побудує та негайно знищить з цього тимчасовий спільний_ptr. Коли ця заява закінчена, thisбуде видалено, а всі наступні посилання будуть звисати.
nobar

2
Автор визнає, що ця відповідь неправильна, тому він, ймовірно, міг просто її видалити. Але він востаннє зареєструвався через 4,5 роки, тому, мабуть, це не зробить - чи може хтось із вищими силами видалити цю червону оселедець?
Tom Goodfellow

якщо ви дивитесь на реалізацію enable_shared_from_this, вона зберігає weak_ptrсебе (заповнений ctor), повертається як a, shared_ptrколи ви телефонуєте shared_from_this. Іншими словами, ви дублюєте те, що enable_shared_from_thisвже передбачено.
mchiasson
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.