De incompatibiliteit komt voort uit de typeeigenschap std::uses_allocator, die evalueert naar false voor de combinatie van std::string en std::pmr::polymorphic_allocator. std::string codeert zijn allocator_type als std::allocator<char>, terwijl std::pmr::vector std::pmr::polymorphic_allocator<char> biedt; dit zijn verschillende, niet-gerelateerde klassen zonder impliciete conversie of erfelijke relatie. Wanneer de container elementen construeert, vraagt het std::uses_allocator_v<T, Alloc> om te bepalen of het de allocator als argument aan de constructor moet doorgeven; omdat deze controle mislukt, beschouwt de vector std::string als allocator-onbekend en roept zijn standaardconstructor aan, die intern gebruikmaakt van globale new en delete, ongeacht de geheugenbron van de vector.
static_assert(!std::uses_allocator_v<std::string, std::pmr::polymorphic_allocator<char>>); // std::pmr::vector zal zijn allocator NIET doorgeven aan std::string
Tijdens de optimalisatie van een financiële risicoberekeningsengine, hebben we een kritieke pad heringericht om std::pmr::monotonic_buffer_resource-ondersteund door stackgeheugen te gebruiken om heap-concurrentie te elimineren. We declareerden std::pmr::vectorstd::string temp_symbols, in de verwachting dat alle tijdelijke symboolnamen uit de monotone buffer zouden komen, maar prestatieprofilering onthulde onverwachte malloc-aanroepen binnen std::string-constructors, wat erop wees dat de geheugenbron volledig werd omzeild.
We overwoogen om elke std::string handmatig te construeren met een expliciete std::pmr::polymorphic_allocator doorgegeven aan zijn constructor, maar dit vereiste het blootstellen van allocatiegegevens aan hogere businesslogica en verhinderde het gebruik van handige modificeerders zoals emplace_back. Een andere benadering bestond uit het maken van een aangepaste stringwrapper die erfde van std::string en een polymorfe allocator accepteerde, maar dit schond het Liskov-substitutiebeginsel en introduceerde het risico van object slicing tijdens containerherallocatie. Uiteindelijk vervingen we std::string door std::pmr::string (een alias voor std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>), die inherent allocator_type als de polymorfe variant verklaart. Dit stelde de vector in staat om automatisch zijn allocator door te geven via het uses_allocator-protocol, wat alle heap-allocaties in het kritieke pad elimineerde en de latentie van microseconden tot honderden nanoseconden verminderde.
Hoe kan een aangepaste klasse compatibel worden gemaakt met std::pmr::polymorphic_allocator als deze interne dynamische allocatie uitvoert, gezien het feit dat het eenvoudig accepteren van een allocatorparameter in zijn constructor onvoldoende is?
Een klasse moet expliciet zijn allocator-bewustzijn adverteren door ofwel een openbare allocator_type type-alias bloot te stellen die converteerbaar is van de gebruikte allocator, of door een constructor te bieden waarvan de eerste parameter std::allocator_arg_t is en de tweede parameter het allocator-type is, gecombineerd met het specialiseren van std::uses_allocator<ClassName, Alloc> om af te leiden van std::true_type. Zonder deze expliciete advertentie gaat std::pmr::vector ervan uit dat de klasse allocator-onbewust is en construeert deze via standaardinitialisatie, waardoor interne allocaties de polymorfe geheugenresource omzeilen.
Waarom lost std::allocator_traits<std::pmr::polymorphic_allocator<T>>::rebind_alloc<U> de incompatibiliteit tussen std::pmr::vector en std::string niet op?
Herverbinding produceert een std::pmr::polymorphic_allocator<U>, die incompatibel blijft met std::allocator<U> omdat het verschillende concrete types zijn zonder conversierelatie. De std::uses_allocator-mechanisme vereist dat het allocator_type van het element hetzelfde is als of converteerbaar van het allocator-type van de container, niet louter herbindbaar naar een ander waarde-type; aangezien std::string std::allocator hardcodeert, verandert het herverbinden van de allocator van de container het verwachte allocator-type van het element niet.
Welks specifiek levensduur risico ontstaat er bij het gebruik van std::pmr::monotonic_buffer_resource met std::pmr::string, en waarom is deze detectie moeilijker dan bij standaard allocators?
Omdat std::pmr::polymorphic_allocator type-geërodeerd is en een pointer naar een basis std::pmr::memory_resource opslaat, kan de compiler levensduurbeperkingen niet afdwingen op compile-tijd. Wanneer een std::pmr::string die verwijst naar een stack-gebaseerde monotonic_buffer_resource wordt verplaatst of gekopieerd naar een langduriger scope, wordt de pointer naar de geheugenresource dangling; in tegenstelling tot std::allocator dat typisch gebruik maakt van de globale heap (altijd geldig), resulteert toegang tot de string na de buffervernietiging in gebruik-na-vrijgeven. Statische analyzers hebben moeite om dit te detecteren omdat de virtuele do_allocate/do_deallocate interface de onderliggende resourcelevensduur voor het typesysteem verbergt.