C++programowanieProgramista C++

Scharakteryzuj typ niezgodności, który uniemożliwia **std::pmr::vector<std::string>** wykorzystanie swojego **std::pmr::polymorphic_allocator** do wewnętrznego przechowywania ciągów znaków?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie

Niezgodność wynika z cechy typowej std::uses_allocator, która ocenia się na false dla kombinacji std::string i std::pmr::polymorphic_allocator. std::string hardcoduje swój allocator_type jako std::allocator<char>, podczas gdy std::pmr::vector oferuje std::pmr::polymorphic_allocator<char>; są to różne, niezwiązane typy klas bez żadnej implikowanej konwersji ani relacji dziedziczenia. Gdy kontener konstruuje elementy, sprawdza std::uses_allocator_v<T, Alloc>, aby ustalić, czy przekazać alokator jako argument konstruktora; ponieważ ten test nie powodzi się, wektor traktuje std::string jako nieświadomy alokacji i wywołuje jego domyślny konstruktor, który wewnętrznie używa globalnego new i delete, niezależnie od zasobu pamięci wektora.

static_assert(!std::uses_allocator_v<std::string, std::pmr::polymorphic_allocator<char>>); // std::pmr::vector NIE przekaże swojego alokatora do std::string

Sytuacja z życia wzięta

Podczas optymalizacji silnika obliczeniowego ryzyka finansowego, przekształciliśmy często używaną część kodu, aby korzystać z std::pmr::monotonic_buffer_resource wspierającego pamięć stosu, aby wyeliminować konkurencję z heapem. Zadeklarowaliśmy std::pmr::vectorstd::string temp_symbols, oczekując, że wszystkie tymczasowe nazwy symboli będą korzystać z monolitycznego bufora, ale profilowanie wydajności ujawniło niespodziewane wywołania malloc wewnątrz konstruktorów std::string, co wskazuje, że zasób pamięci był całkowicie omijany.

Rozważaliśmy ręczne konstruowanie każdego std::string z eksplicytnym std::pmr::polymorphic_allocator przekazywanym do jego konstruktora, ale to wymagało ujawnienia szczegółów alokacji wyższym warstwom logiki biznesowej i uniemożliwiało korzystanie z wygodnych modyfikatorów takich jak emplace_back. Inne podejście polegało na stworzeniu niestandardowego opakowania dla ciągów, które dziedziczyło z std::string i akceptowało alokator polimorficzny, ale to naruszało zasadę podstawienia Liskova i wprowadzało ryzyko odcinania obiektów podczas ponownej alokacji kontenera. Ostatecznie zastąpiliśmy std::string std::pmr::string (alias dla std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>), co z natury ogłasza allocator_type jako polimorficzną wersję. To umożliwiło wektorowi automatyczne propagowanie swojego alokatora przez protokół uses_allocator, eliminując wszystkie alokacje heap w ważnej ścieżce i redukując latencję z mikrosekund do setek nanosekund.

Co często umyka kandydatom

Jak można uczynić niestandardową klasę zgodną z std::pmr::polymorphic_allocator, jeśli wykonuje wewnętrzną alokację dynamiczną, biorąc pod uwagę, że samo przyjmowanie parametru alokatora w jej konstruktorze jest niewystarczające?

Klasa musi wyraźnie ogłosić swoją świadomość alokacji, albo poprzez ujawnienie publicznego aliasu typu allocator_type, który jest konwertowalny z używanego alokatora, albo przez dostarczenie konstruktora, którego pierwszy parametr to std::allocator_arg_t, a drugi parametr to typ alokatora, w połączeniu z specjalizowaniem std::uses_allocator<ClassName, Alloc>, aby dziedziczyć z std::true_type. Bez tego wyraźnego ogłoszenia, std::pmr::vector zakłada, że klasa jest nieświadoma alokacji i konstruuje ją przez domyślną inicjalizację, co powoduje, że wszelkie wewnętrzne alokacje omijają polimorficzny zasób pamięci.

Dlaczego std::allocator_traits<std::pmr::polymorphic_allocator<T>>::rebind_alloc<U> nie rozwiązuje niezgodności między std::pmr::vector a std::string?

Rebinding wytwarza std::pmr::polymorphic_allocator<U>, który pozostaje niezgodny z std::allocator<U> ponieważ są to różne konkretne typy bez relacji konwersji. Mechanizm std::uses_allocator wymaga, aby allocator_type elementu był taki sam lub konwertowalny z typem alokatora kontenera, a nie tylko rebindowalny do innego typu wartości; ponieważ std::string hardcoduje std::allocator, rebindowanie alokatora kontenera nie zmienia oczekiwanego typu alokatora elementu.

Jakie konkretne ryzyko długości życia powstaje przy używaniu std::pmr::monotonic_buffer_resource z std::pmr::string, i dlaczego wykrycie tego jest trudniejsze niż z standardowymi alokatorami?

Ponieważ std::pmr::polymorphic_allocator jest zacierany typowo i przechowuje wskaźnik do bazowego std::pmr::memory_resource, kompilator nie może egzekwować ograniczeń długości życia w czasie kompilacji. Kiedy std::pmr::string odnoszący się do opartego na stosie monotonic_buffer_resource jest przenoszony lub kopiowany do dłużej żyjącego zakresu, wskaźnik do zasobu pamięci staje się zwisający; w przeciwieństwie do std::allocator, który zazwyczaj używa globalnego heapu (zawsze ważnego), dostęp do ciągu po zniszczeniu bufora kończy się wykorzystaniem po zwolnieniu pamięci. Statyczne analizatory mają trudności z wykryciem tego, ponieważ wirtualny interfejs do_allocate/do_deallocate ukrywa długość życia podlegającego zasobu przed systemem typów.