L'incompatibilità deriva dal tratto di tipo std::uses_allocator, che restituisce false per la combinazione di std::string e std::pmr::polymorphic_allocator. std::string fissa il suo allocator_type come std::allocator<char>, mentre std::pmr::vector fornisce std::pmr::polymorphic_allocator<char>; questi sono tipi di classe distinti e non correlati senza relazione di conversione implicita o eredità. Quando il contenitore costruisce gli elementi, interroga std::uses_allocator_v<T, Alloc> per determinare se passare l'allocatore come argomento del costruttore; poiché questo controllo fallisce, il vettore tratta std::string come non consapevole dell'allocatore e invoca il suo costruttore di default, che utilizza internamente new e delete globali indipendentemente dalla risorsa di memoria del vettore.
static_assert(!std::uses_allocator_v<std::string, std::pmr::polymorphic_allocator<char>>); // std::pmr::vector NON passerà il suo allocatore a std::string
Durante l'ottimizzazione di un motore di calcolo del rischio finanziario, abbiamo riprogettato un percorso critico per utilizzare std::pmr::monotonic_buffer_resource supportato da memoria stack per eliminare la contesa heap. Abbiamo dichiarato std::pmr::vectorstd::string temp_symbols aspettandoci che tutti i nomi dei simboli temporanei attingessero dal buffer monotono, ma il profilo delle prestazioni ha rivelato chiamate malloc inaspettate all'interno dei costruttori di std::string, indicando che la risorsa di memoria veniva completamente bypassata.
Abbiamo considerato di costruire manualmente ogni std::string con un esplicito std::pmr::polymorphic_allocator passato al suo costruttore, ma ciò avrebbe richiesto di esporre i dettagli di allocazione alla logica aziendale di livello superiore e avrebbe impedito l'utilizzo di modificatori convenienti come emplace_back. Un altro approccio ha coinvolto la creazione di un wrapper stringa personalizzato che ereditava da std::string e accettava un allocatore polimorfico, ma questo violava il principio di sostituzione di Liskov e introduceva rischi di slicing degli oggetti durante la riallocazione del contenitore. Alla fine, abbiamo sostituito std::string con std::pmr::string (un alias per std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>), che dichiarava inherentemente allocator_type come la variante polimorfica. Questo ha permesso al vettore di propagare automaticamente il suo allocatore attraverso il protocollo uses_allocator, eliminando tutte le allocazioni heap nel percorso critico e riducendo la latenza da microsecondi a centinaia di nanosecondi.
Come può una classe personalizzata essere resa compatibile con std::pmr::polymorphic_allocator se esegue allocazioni dinamiche interne, dato che semplicemente accettare un parametro allocatore nel suo costruttore è insufficiente?
Una classe deve esplicitamente pubblicizzare la sua consapevolezza dell'allocatore esponendo un alias di tipo pubblico allocator_type convertibile dall'allocatore in uso, oppure fornendo un costruttore il cui primo parametro è std::allocator_arg_t e il secondo parametro è il tipo di allocatore, combinato con la specializzazione di std::uses_allocator<ClassName, Alloc> per derivare da std::true_type. Senza questa pubblicità esplicita, std::pmr::vector assume che la classe non sia consapevole dell'allocatore e la costruisce tramite inizializzazione di default, causando qualsiasi allocazione interna di bypassare la risorsa di memoria polimorfica.
Perché std::allocator_traits<std::pmr::polymorphic_allocator<T>>::rebind_alloc<U> non risolve l'incompatibilità tra std::pmr::vector e std::string?
La riassegnazione produce un std::pmr::polymorphic_allocator<U>, che rimane incompatibile con std::allocator<U> perché sono tipi concreti distinti senza relazione di conversione. Il meccanismo std::uses_allocator richiede che l'allocator_type dell'elemento sia lo stesso o convertibile dal tipo di allocatore del contenitore, non semplicemente riassegnabile a un diverso tipo di valore; poiché std::string fissa std::allocator, riassegnare l'allocatore del contenitore non cambia il tipo di allocatore atteso dall'elemento.
Quale specifico rischio di durata sorge quando si utilizza std::pmr::monotonic_buffer_resource con std::pmr::string, e perché questa rilevazione è più difficile rispetto agli allocatori standard?
Poiché std::pmr::polymorphic_allocator è cancellato per tipo e memorizza un puntatore a una base std::pmr::memory_resource, il compilatore non può imporre vincoli di durata a tempo di compilazione. Quando un std::pmr::string che fa riferimento a una monotonic_buffer_resource basata su stack viene spostato o copiato in uno scope di durata più lunga, il puntatore alla risorsa di memoria diventa pendente; a differenza di std::allocator che utilizza tipicamente l'heap globale (sempre valido), accedere alla stringa dopo la distruzione del buffer risulta in un uso dopo la liberazione. Gli analizzatori statici faticano a rilevare questo perché l'interfaccia virtuale do_allocate/do_deallocate nasconde la durata della risorsa sottostante dal sistema di tipi.