L'incompatibilité provient du trait de type std::uses_allocator, qui évalue à false pour la combinaison de std::string et std::pmr::polymorphic_allocator. std::string code en dur son allocator_type en tant que std::allocator<char>, tandis que std::pmr::vector fournit std::pmr::polymorphic_allocator<char> ; ces types de classe sont distincts et non liés, sans relation d'héritage ou de conversion implicite. Lorsque le conteneur construit des éléments, il interroge std::uses_allocator_v<T, Alloc> pour déterminer s'il doit passer l'allocateur comme argument au constructeur ; comme cette vérification échoue, le vecteur traite std::string comme indépendant de l'allocateur et invoque son constructeur par défaut, qui utilise en interne new et delete globaux, indépendamment de la ressource mémoire du vecteur.
static_assert(!std::uses_allocator_v<std::string, std::pmr::polymorphic_allocator<char>>); // std::pmr::vector ne PASSERA PAS son allocateur à std::string
Lors de l'optimisation d'un moteur de calcul de risque financier, nous avons refactorisé un chemin critique pour utiliser std::pmr::monotonic_buffer_resource soutenu par de la mémoire de pile afin d'éliminer la contention de la mémoire dynamique. Nous avons déclaré std::pmr::vectorstd::string temp_symbols en nous attendant à ce que tous les noms de symboles temporaires tirent de la mémoire tampon monotone, mais le profilage des performances a révélé des appels malloc inattendus dans les constructeurs de std::string, indiquant que la ressource mémoire était complètement contournée.
Nous avons envisagé de construire manuellement chaque std::string avec un std::pmr::polymorphic_allocator passé à son constructeur, mais cela nécessitait d'exposer les détails d'allocation à une logique métier de niveau supérieur et empêchait l'utilisation de modificateurs pratiques comme emplace_back. Une autre approche a consisté à créer un wrapper de chaîne personnalisé qui hérite de std::string et accepte un allocateur polymorphe, mais cela violait le principe de substitution de Liskov et introduisait des risques de découpage d'objet lors de la réallocation du conteneur. Nous avons finalement remplacé std::string par std::pmr::string (un alias pour std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>), qui déclare intrinsèquement allocator_type comme variant polymorphe. Cela a permis au vecteur de propager automatiquement son allocateur par le biais du protocole uses_allocator, éliminant toutes les allocations de mémoire dynamique dans le chemin critique et réduisant la latence de microsecondes à des centaines de nanosecondes.
Comment une classe personnalisée peut-elle être rendue compatible avec std::pmr::polymorphic_allocator si elle effectue une allocation dynamique interne, étant donné que l'acceptation simple d'un paramètre allocateur dans son constructeur est insuffisante ?
Une classe doit explicitement annoncer sa prise en charge de l'allocateur en exposant soit un alias de type public allocator_type convertible depuis l'allocateur en cours d'utilisation, soit en fournissant un constructeur dont le premier paramètre est std::allocator_arg_t et le deuxième paramètre est le type d'allocation, combiné avec la spécialisation de std::uses_allocator<ClassName, Alloc> pour hériter de std::true_type. Sans cette annonce explicite, std::pmr::vector suppose que la classe est indépendante de l'allocateur et la construit par initialisation par défaut, entraînant ainsi des allocations internes contournant la ressource mémoire polymorphe.
Pourquoi std::allocator_traits<std::pmr::polymorphic_allocator<T>>::rebind_alloc<U> ne résout-il pas l'incompatibilité entre std::pmr::vector et std::string ?
Le rebinding produit un std::pmr::polymorphic_allocator<U>, qui reste incompatible avec std::allocator<U> car ce sont des types concrets distincts sans relation de conversion. Le mécanisme std::uses_allocator exige que le allocator_type de l'élément soit le même que celui du type d’allocateur du conteneur, ou convertible à partir de celui-ci, et pas simplement réassignable à un type de valeur différent ; puisque std::string hardcode std::allocator, le rebind de l'allocateur du conteneur ne change pas le type d’allocation attendu de l’élément.
Quel risque de durée de vie spécifique survient lors de l'utilisation de std::pmr::monotonic_buffer_resource avec std::pmr::string, et pourquoi cette détection est-elle plus difficile qu'avec des allocateurs standard ?
Parce que std::pmr::polymorphic_allocator est effacé de type et stocke un pointeur vers une base std::pmr::memory_resource, le compilateur ne peut pas imposer de contraintes de durée de vie à la compilation. Lorsqu'un std::pmr::string faisant référence à un monotonic_buffer_resource basé sur la pile est déplacé ou copié dans un espace de durée de vie plus long, le pointeur vers la ressource mémoire devient suspicieux ; contrairement à std::allocator qui utilise généralement le tas global (toujours valide), accéder à la chaîne après la destruction du tampon entraîne une utilisation après libération. Les analyseurs statiques ont du mal à détecter cela car l'interface virtuelle do_allocate/do_deallocate cache la durée de vie de la ressource sous-jacente du système de type.