Jaka jest użyteczność `enable_shared_from_this`?

349

Natknąłem się enable_shared_from_thisna czytając przykłady Boost.Asio i po przeczytaniu dokumentacji wciąż jestem zagubiony w tym, jak należy to właściwie wykorzystać. Czy ktoś może mi podać przykład, a wyjaśnienie, kiedy korzystam z tej klasy, ma sens.

fido
źródło

Odpowiedzi:

362

Umożliwia uzyskanie prawidłowej shared_ptrinstancji this, gdy wszystko, co masz, to this. Bez niego nie byłoby sposobu, aby dostać się shared_ptrdo this, chyba że masz go już jako członek. Ten przykład z dokumentacji doładowania dla 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
}

Metoda f()zwraca poprawną wartość shared_ptr, mimo że nie miała instancji elementu. Pamiętaj, że nie możesz tego po prostu zrobić:

class Y: public enable_shared_from_this<Y>
{
public:

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

Współdzielony wskaźnik, który to zwróci, będzie miał inną liczbę odniesień niż „właściwy”, a jeden z nich straci i utrzyma wiszące odniesienie, gdy obiekt zostanie usunięty.

enable_shared_from_thisstał się częścią standardu C ++ 11. Możesz także pobrać go stamtąd, a także z doładowania.

1800 INFORMACJI
źródło
202
+1. Kluczową kwestią jest to, że „oczywista” technika po prostu zwracania shared_ptr <Y> (this) jest zepsuta, ponieważ kończy się to tworzeniem wielu różnych obiektów shared_ptr z osobnymi liczbami referencyjnymi. Z tego powodu nigdy nie wolno tworzyć więcej niż jednego shared_ptr z tego samego surowego wskaźnika .
j_random_hacker
3
Należy zauważyć, że w C ++ 11 i późniejszych jest całkowicie poprawne użycie std::shared_ptrkonstruktora na surowym wskaźniku, jeśli dziedziczy std::enable_shared_from_this. Nie wiem, czy semantyka wzmocnienia została zaktualizowana w celu obsługi tego.
Matthew
6
@MatthewHolder Czy masz na to wycenę? Na stronie cppreference.com przeczytałem „Konstruowanie std::shared_ptrobiektu, który jest już zarządzany przez inny obiekt std::shared_ptr, nie sprawdzi wewnętrznej słabej referencji przechowywanej wewnętrznie, a tym samym doprowadzi do nieokreślonego zachowania”. ( en.cppreference.com/w/cpp/memory/enable_shared_from_this )
Thorbjørn Lindeijer
5
Dlaczego nie możesz tego zrobić shared_ptr<Y> q = p?
Dan M.,
2
@ ThorbjørnLindeijer, masz rację, to C ++ 17 i nowsze wersje. Niektóre implementacje były zgodne z semantyką C ++ 16 przed jej wydaniem. Należy używać poprawnej obsługi C ++ 11 do C ++ 14 std::make_shared<T>.
Matthew
198

z artykułu dr Dobbs na temat słabych wskaźników, myślę, że ten przykład jest łatwiejszy do zrozumienia (źródło: http://drdobbs.com/cpp/184402026 ):

... taki kod nie działa poprawnie:

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

Żaden z dwóch shared_ptrobiektów nie wie o drugim, więc oba będą próbowały uwolnić zasób, gdy zostaną zniszczone. To zwykle prowadzi do problemów.

Podobnie, jeśli funkcja członka potrzebuje shared_ptrobiektu będącego właścicielem obiektu, do którego jest wywoływana, nie może po prostu utworzyć obiektu w locie:

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;
}

Ten kod ma ten sam problem, co we wcześniejszym przykładzie, chociaż w bardziej subtelnej formie. Podczas budowy shared_ptobiekt r sp1jest właścicielem nowo przydzielonego zasobu. Kod wewnątrz funkcji S::dangerousskładowej nie wie o tym shared_ptrobiekcie, więc shared_ptrobiekt, który zwraca, różni się od niego sp1. Kopiowanie nowego shared_ptrobiektu do sp2nie pomaga; gdy sp2wyjdzie poza zakres, zwolni zasób, a gdy sp1wyjdzie poza zakres, ponownie zwolni zasób.

Sposobem uniknięcia tego problemu jest użycie szablonu klasy enable_shared_from_this. Szablon przyjmuje jeden argument typu szablonu, który jest nazwą klasy definiującej zarządzany zasób. Ta klasa musi z kolei pochodzić publicznie z szablonu; lubię to:

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;
}

Gdy to zrobisz, pamiętaj, że obiekt, do którego dzwonisz, shared_from_thismusi być własnością shared_ptrobiektu. To nie zadziała:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}
Artashes Aghajanyan
źródło
15
Dzięki, to pokazuje, że problem został rozwiązany lepiej niż obecnie akceptowana odpowiedź.
goertzenator
2
+1: Dobra odpowiedź. Nawiasem shared_ptr<S> sp1(new S);mówiąc , zamiast tego może być preferowane użycie shared_ptr<S> sp1 = make_shared<S>();, patrz na przykład stackoverflow.com/questions/18301511/…
Arun
4
Jestem prawie pewien, że ostatnia linijka powinna być przeczytana, shared_ptr<S> sp2 = p->not_dangerous();ponieważ pułapką jest to, że musisz utworzyć shared_ptr w normalny sposób, zanim zadzwonisz shared_from_this()po raz pierwszy! Naprawdę łatwo się pomylić! W wersjach wcześniejszych niż C ++ 17 UB może wywoływać shared_from_this()przed utworzeniem dokładnie jednego pliku shared_ptr w normalny sposób: auto sptr = std::make_shared<S>();lub shared_ptr<S> sptr(new S());. Na szczęście od C ++ 17 robienie tego rzuci.
AnorZaken,
2
ZŁY PRZYKŁAD: S* s = new S(); shared_ptr<S> ptr = s->not_dangerous();<- Dozwolone jest wywoływanie shared_from_this tylko na wcześniej współużytkowanym obiekcie, tj. Na obiekcie zarządzanym przez std :: shared_ptr <T>. W przeciwnym razie zachowanie jest niezdefiniowane (dopóki C ++ 17) nie zostanie wyrzucone std :: bad_weak_ptr (przez konstruktora shared_ptr z domyślnie skonstruowanego słaby_this) (od C ++ 17). . Tak więc rzeczywistość jest taka, że ​​należy ją nazywać always_dangerous(), ponieważ potrzebujesz wiedzy, czy została już udostępniona, czy nie.
AnorZaken,
2
@AnorZaken Dobra uwaga. Byłoby przydatne, gdybyś przesłał prośbę o edycję, aby wprowadzić tę poprawkę. Właśnie to zrobiłem. Inną przydatną rzeczą byłoby, aby plakat nie wybrał subiektywnych, kontekstowych nazw metod!
underscore_d
30

Oto moje wyjaśnienie z perspektywy orzechów (pierwsza odpowiedź nie „kliknęła” ze mną). * Zauważ, że jest to wynik badania źródła shared_ptr i enable_shared_from_thth, który jest dostarczany z Visual Studio 2012. Być może inne kompilatory implementują enable_shared_from_this inaczej inaczej ... *

enable_shared_from_this<T>dodaje prywatną weak_ptr<T>instancję, do Tktórej przypisano „ jedną prawdziwą liczbę referencji ” dla instancji T.

Tak więc, kiedy po raz pierwszy tworzysz shared_ptr<T>nowy na T *, wewnętrzny słaby_ptr T * jest inicjalizowany z przeliczeniem 1. Nowy w shared_ptrzasadzie się na to powraca weak_ptr.

Tnastępnie w swoich metodach może wywoływać, shared_from_thisaby uzyskać instancję shared_ptr<T>tego zaplecza na tej samej wewnętrznie przechowywanej liczbie referencji . W ten sposób zawsze masz jedno miejsce, w którym T*przechowywana jest liczba odwołań, a nie wiele shared_ptrwystąpień, które nie znają się nawzajem, i każdy uważa, że ​​to one są shared_ptrodpowiedzialne za przeliczanie Ti usuwanie go, gdy ich odwołanie -licznik osiąga zero.

mackenir
źródło
1
Jest to poprawne, a naprawdę istotną częścią jest So, when you first create...to, że jest to wymóg (jak mówisz, słaby_ptr nie jest inicjowany, dopóki nie przekażesz wskaźnika obiektów do ctora shared_ptr!), A to wymaganie jest tam, gdzie rzeczy mogą się potwornie źle pójść, jeśli jesteś nieostrożny. Jeśli przed wywołaniem nie utworzysz pliku shared_ptr shared_from_this, otrzymasz UB - podobnie, jeśli utworzysz więcej niż jeden plik shared_ptr, otrzymasz również UB. Musisz jakoś upewnić się, że utworzyłeś shared_ptr dokładnie raz.
AnorZaken,
2
Innymi słowy, cała idea „ enable_shared_from_thiskruchego” jest na początku krucha, ponieważ chodzi o to, aby móc uzyskać shared_ptr<T>od T*, ale w rzeczywistości, kiedy dostajesz wskaźnik T* t, generalnie nie jest bezpiecznie zakładać, że cokolwiek jest już udostępnione lub nie, i błędne zgadywanie to UB.
AnorZaken,
wewnętrzny słaby_ptr jest inicjowany z przeliczeniem 1 ” słaby ptr do T nie jest inteligentnym ptr do T. Słaby ptr jest posiadającym sprytnym odwołaniem do wystarczającej ilości informacji, aby utworzyć ptr będący właścicielem, który jest „kopią” innego ptr będącego właścicielem. Słaby ptr nie ma wartości ref. Ma dostęp do liczby referencji, jak wszystkie posiadane referencje.
ciekawy
3

Zauważ, że użycie boost :: intrusive_ptr nie cierpi z powodu tego problemu. Jest to często wygodniejszy sposób na obejście tego problemu.

Blais
źródło
Tak, ale enable_shared_from_thisumożliwia pracę z interfejsem API, który specjalnie akceptuje shared_ptr<>. Moim zdaniem taki interfejs API zwykle robi „Zrób to źle” (ponieważ lepiej jest pozwolić, aby coś wyżej w stosie posiadało pamięć), ale jeśli jesteś zmuszony do pracy z takim interfejsem API, jest to dobra opcja.
cdunn2001
2
Lepiej pozostać w granicach standardu, jak możesz.
Siergiej
3

Dokładnie tak samo jest w c ++ 11 i późniejszych wersjach: od tego momentu ma możliwość powrotu thisjako wskaźnik wspólnythis daje surowy wskaźnik.

innymi słowy, pozwala zmienić kod w ten sposób

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

private:

    Node * m_parent = nullptr;
};           

zaangażowany w to:

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;
};           
mchiasson
źródło
Działa to tylko wtedy, gdy obiektami tymi zawsze zarządza shared_ptr. Możesz zmienić interfejs, aby upewnić się, że tak jest.
ciekawy
1
Masz absolutną rację @curiousguy. To oczywiste. Lubię też pisać na maszynie wszystkie moje shared_ptr, aby poprawić czytelność podczas definiowania moich publicznych interfejsów API. Na przykład zamiast tego std::shared_ptr<Node> getParent const()normalnie odsłoniłbym to jako NodePtr getParent const()zamiast. Jeśli absolutnie potrzebujesz dostępu do wewnętrznego surowego wskaźnika (najlepszy przykład: radzenie sobie z biblioteką C), jest std::shared_ptr<T>::getna to coś, o czym nienawidzę wspominać, ponieważ używam tego surowego wskaźnika zbyt wiele razy z niewłaściwego powodu.
mchiasson,
-3

Innym sposobem jest dodanie weak_ptr<Y> m_stubczłonka do class Y. Następnie napisz:

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

Przydatne, gdy nie możesz zmienić klasy, z której wywodzisz (np. Rozszerzając bibliotekę innych osób). Nie zapomnij zainicjować członka, np. Przezm_stub = shared_ptr<Y>(this) , że jest on ważny nawet podczas konstruktora.

Jest OK, jeśli istnieje więcej kodów pośredniczących w hierarchii dziedziczenia, nie zapobiegnie to zniszczeniu obiektu.

Edycja: Jak poprawnie wskazał nobar użytkownika, kod zniszczyłby obiekt Y po zakończeniu przypisania i zniszczeniu zmiennych tymczasowych. Dlatego moja odpowiedź jest nieprawidłowa.

PetrH
źródło
4
Jeśli twoim zamiarem jest stworzenie shared_ptr<>czegoś, co nie usunie jego pointee, jest to przesada. Możesz po prostu powiedzieć, return shared_ptr<Y>(this, no_op_deleter);gdzie no_op_deleterjest obiekt jednoargumentowy, który Y*nic nie bierze i nie robi.
John Zwinck,
2
Wydaje się mało prawdopodobne, że jest to działające rozwiązanie. m_stub = shared_ptr<Y>(this)zbuduje i natychmiast zniszczy tymczasowy share_ptr z tego. Po zakończeniu tej instrukcji thiszostaną usunięte, a wszystkie kolejne odniesienia będą zwisały.
nobar
2
Autor przyznaje, że ta odpowiedź jest błędna, więc prawdopodobnie mógł ją po prostu usunąć. Ale ostatnio zalogował się w 4,5 roku, więc raczej tego nie zrobi - czy ktoś o wyższych mocach mógłby usunąć ten czerwony śledź?
Tom Goodfellow,
jeśli spojrzysz na implementację enable_shared_from_this, zachowuje ona weak_ptrswoją wartość (wypełnioną przez ctor), zwracaną jako shared_ptrkiedy zadzwonisz shared_from_this. Innymi słowy, powielasz to, co enable_shared_from_thisjuż zapewnia.
mchiasson,