std::enable_shared_from_this jest klasą bazową mixin, która enkapsuluje prywatny, modyfikowalny człon std::weak_ptr<T>, zazwyczaj nazywany weak_this. Podczas konstrukcji obiektu pochodnego ten wewnętrzny weak_ptr przechodzi przez domyślną konstrukcję, pozostawiając go w pustym (wygaśniętym) stanie. Krytyczny szczegół architektoniczny polega na tym, że inicjalizacja wskaźnika wewnętrznego do odniesienia do bloku sterującego zachodzi wyłącznie wewnątrz konstruktora std::shared_ptr po zakończeniu konstruktora obiektu zarządzanego. W związku z tym wywołanie shared_from_this() w ciele konstruktora próbuje wywołać lock() na pustym weak_ptr, co od C++17 nakazuje zgłoszenie wyjątku std::bad_weak_ptr (lub niezdefiniowane zachowanie w wcześniejszych standardach), ponieważ infrastruktura współdzielonej własności wymagana do wydania nowych referencji nie została jeszcze ustalona.
Kontext:
Platforma handlowa o wysokiej częstotliwości zaimplementowała klasę MarketDataHandler, aby zarządzać trwałymi połączeniami TCP z giełdami. Aby zagwarantować, że handler pozostanie aktywny podczas asynchronicznych operacji odczytu/zapisu soketów, klasa dziedziczyła z std::enable_shared_from_this<MarketDataHandler>. Konstruktor przyjmował parametry połączenia i natychmiast inicjował asynchroniczną operację odczytu, przekazując shared_from_this() jako handler zakończenia do pętli zdarzeń Boost.Asio.
Problem:
Podczas testów integracyjnych aplikacja zawieszała się natychmiast po nawiązaniu połączenia z niełapanymi wyjątkami std::bad_weak_ptr, które kończyły proces. Zespół developerski zakładał, że ponieważ obiekt klasy bazowej std::enable_shared_from_this jest konstruktywny przed wykonaniem ciała konstruktora klasy pochodnej, mechanizm śledzenia wewnętrznego będzie gotowy do natychmiastowego użycia. Nie uwzględnili różnicy czasowej między konstrukcją obiektu a zakończeniem opakowania std::shared_ptr, co pozostawia wewnętrzny weak_ptr nieinicjowany do momentu zakończenia wyrażenia fabrycznego.
Rozważane alternatywne rozwiązania:
Inicjalizacja w dwóch fazach za pomocą post_construct():
Zrefaktoryzować klasę, aby przenieść całą logikę inicjacji asynchronicznej z konstruktora do osobnej publicznej metody post_construct(). Wywołujący najpierw tworzyłby std::shared_ptr<MarketDataHandler> za pomocą std::make_shared, a następnie natychmiast wywoływał post_construct() na wyniku przed zwróceniem wskaźnika do systemu.
post_construct(), co prowadzi do subtelnych błędów, w których handler nigdy nie zaczyna przetwarzać danych.Surowy wskaźnik z zewnętrznymi gwarancjami żywotności:
Przekazać surowy wskaźnik this do systemu asynchronicznego I/O i prowadzić osobny globalny rejestr aktywnych połączeń z kluczami std::shared_ptr, sprawdzając przynależność do rejestru przy każdym wykonywaniu callbacku.
shared_from_this().Statyczna metoda fabryczna z prywatnym konstruktorem:
Uczynić wszystkie konstruktory prywatnymi i zapewnić publiczną metodę statyczną create(), zwracającą std::shared_ptr<MarketDataHandler>. Wewnątrz create() metoda najpierw tworzy obiekt za pomocą std::make_shared, a następnie inicjuje operacje asynchroniczne za pomocą utworzonego wskaźnika współdzielonego, zanim go zwróci do wywołującego.
std::make_shared z prywatnymi konstruktorami, chyba że fabryka zostanie zadeklarowana jako przyjaciel; wymaga nieco bardziej rozbudowanej składni (MarketDataHandler::create() w porównaniu do std::make_shared<MarketDataHandler>()).Wybrane rozwiązanie:
Model statycznej fabryki został wybrany, ponieważ wyeliminował możliwość wywołania shared_from_this() na obiekcie, który nie jest do niego przypisany. Ograniczając konstrukcję do metody create(), zapewniliśmy, że blok kontrolny std::shared_ptr był zawsze w pełni skonstruowany i zainicjował wewnętrzny weak_ptr, zanim którakolwiek metoda mogła spróbować wydać dodatkowe referencje.
Wynik:
Refaktoryzacja wyeliminowała wszystkie awarie startupowe. Kod został przyjęty zgodnie z solidnym wzorem do asynchronicznego tworzenia obiektów, który był stosowany jednolicie w warstwie sieciowej. Wytyczne do przeglądów kodu zostały zaktualizowane, aby zabraniały jakichkolwiek wywołań shared_from_this() poza metodami wywoływanymi po zakończeniu konstrukcji fabrycznej, znacznie ograniczając wskaźniki wad związanych z żywotnością.
Pytanie: Czy shared_from_this() zwiększa liczbę referencji i jak współdziała z blokiem kontrolnym?
Odpowiedź:
shared_from_this() nie tworzy nowego bloku kontrolnego. Zamiast tego uzyskuje dostęp do wewnętrznego, modyfikowalnego członu std::weak_ptr<T> przechowywanego w klasie bazowej std::enable_shared_from_this i wywołuje lock() na nim. Ta operacja atomowo sprawdza, czy blok kontrolny nadal istnieje i, jeśli tak, zwiększa licznik silnych referencji związanych z istniejącym blokiem kontrolnym, zwracając nową instancję std::shared_ptr, która dzieli własność. Jeśli obiekt został już zniszczony (wygasły wskaźnik słaby), lock() zwraca pusty std::shared_ptr. Kandydaci często błędnie zakładają, że shared_from_this() po prostu zwraca kopię jakiegoś wewnętrznego shared_ptr, nie zauważając, że faktycznie promuje słabą referencję do silnej, co jest kluczowe, aby unikać scenariuszy "podwójnej własności", gdzie dwa niezależne instancje std::shared_ptr mogłyby w przeciwnym razie śledzić ten sam obiekt z oddzielnymi licznikami referencji.
Pytanie: Czy klasa może dziedziczyć z std::enable_shared_from_this<T> wiele razy, czy przez wiele ścieżek w hierarchii diamentów?
Odpowiedź:
Klasa nie może bezpośrednio dziedziczyć z std::enable_shared_from_this<T> wiele razy dla tego samego T, ponieważ stworzyłoby to niejednoznaczne podobiekty klas bazowych. Jednak klasa Derived powinna dziedziczyć wyłącznie z std::enable_shared_from_this<Derived>, nie z wersji klasy bazowej. Krytyczny szczegół, który kandydaci pomijają, dotyczy dziedziczenia wirtualnego: jeśli Base dziedziczy z std::enable_shared_from_this<Base>, a Derived dziedziczy z Base, wywołanie shared_from_this() na wskaźniku Base z wnętrza Derived działa poprawnie, ponieważ wewnętrzny weak_ptr jest inicjowany do punktowania na najbardziej pochodny obiekt. Jednak jeśli Derived również dziedziczy publicznie z std::enable_shared_from_this<Derived>, to tworzy dwa oddzielne człony weak_ptr, co prowadzi do zamieszania, który z nich zostanie zainicjowany. Standard nakazuje, że inicjalizacja przez konstruktory std::shared_ptr szczególnie szuka specjalizacji std::enable_shared_from_this; posiadanie wielu niezależnych członów weak_ptr skutkuje zainicjowaniem tylko jednego (zwykle tego, który jest związany z typem statycznym, używanym do stworzenia pierwszego std::shared_ptr), co potencjalnie pozostawia inne puste i powoduje, że następne wywołania shared_from_this() kończą się niepowodzeniem.
Pytanie: Dlaczego std::make_shared w porównaniu do std::shared_ptr<T>(new T) jest nieistotny dla bezpieczeństwa shared_from_this() podczas konstrukcji?
Odpowiedź:
Obie strategie alokacji ostatecznie wywołują konstruktor std::shared_ptr, który wykrywa klasę bazową std::enable_shared_from_this za pomocą metaprogramowania szablonów. Inicjalizacja wewnętrznego weak_ptr zachodzi ściśle wewnątrz logiki konstruktora std::shared_ptr, a nie podczas wykonywania new T czy wewnątrz fazy konstrukcji obiektu make_shared. Dokładnie, make_shared alokuje miejsce, konstruuje obiekt T (podczas którego weak_ptr pozostaje pusty), a dopiero potem konstruktor std::shared_ptr inicjalizuje weak_ptr, aby wskazywał na nowo utworzony blok kontrolny. Kandydaci często zakładają, że make_shared może w jakiś sposób "przygotować" obiekt wcześniej z powodu swojej optymalizacji jednorazowej alokacji, lecz standard gwarantuje, że shared_from_this() jest niebezpieczne do wywołania z ciała konstruktora niezależnie od tego, która funkcja fabryczna została użyta, ponieważ przypisanie weak_ptr następuje ściśle po zakończeniu konstruktora T.