The incompatibility stems from the std::uses_allocator type trait, which evaluates to false for the combination of std::string and std::pmr::polymorphic_allocator. std::string hardcodes its allocator_type as std::allocator<char>, whereas std::pmr::vector provides std::pmr::polymorphic_allocator<char>; these are distinct, unrelated class types with no implicit conversion or inheritance relationship. When the container constructs elements, it queries std::uses_allocator_v<T, Alloc> to determine whether to pass the allocator as a constructor argument; because this check fails, the vector treats std::string as allocator-unaware and invokes its default constructor, which internally uses global new and delete regardless of the vector's memory resource.
static_assert(!std::uses_allocator_v<std::string, std::pmr::polymorphic_allocator<char>>); // std::pmr::vector will NOT pass its allocator to std::string
During optimization of a financial risk calculation engine, we refactored a hot path to use std::pmr::monotonic_buffer_resource backed by stack memory to eliminate heap contention. We declared std::pmr::vectorstd::string temp_symbols expecting all temporary symbol names to draw from the monotonic buffer, but performance profiling revealed unexpected malloc calls within std::string constructors, indicating the memory resource was being bypassed entirely.
We considered manually constructing every std::string with an explicit std::pmr::polymorphic_allocator passed to its constructor, but this required exposing allocation details to higher-level business logic and prevented use of convenient modifiers like emplace_back. Another approach involved creating a custom string wrapper that inherited from std::string and accepted a polymorphic allocator, but this violated the Liskov substitution principle and introduced object slicing risks during container reallocation. We ultimately replaced std::string with std::pmr::string (an alias for std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>), which inherently declares allocator_type as the polymorphic variant. This enabled the vector to automatically propagate its allocator through the uses_allocator protocol, eliminating all heap allocations in the hot path and reducing latency from microseconds to hundreds of nanoseconds.
How can a custom class be made compatible with std::pmr::polymorphic_allocator if it performs internal dynamic allocation, given that simply accepting an allocator parameter in its constructor is insufficient?
A class must explicitly advertise its allocator-awareness by either exposing a public allocator_type type alias convertible from the allocator in use, or by providing a constructor whose first parameter is std::allocator_arg_t and second parameter is the allocator type, combined with specializing std::uses_allocator<ClassName, Alloc> to derive from std::true_type. Without this explicit advertisement, std::pmr::vector assumes the class is allocator-unaware and constructs it via default initialization, causing any internal allocations to bypass the polymorphic memory resource.
Why does std::allocator_traits<std::pmr::polymorphic_allocator<T>>::rebind_alloc<U> not resolve the incompatibility between std::pmr::vector and std::string?
Rebinding produces a std::pmr::polymorphic_allocator<U>, which remains incompatible with std::allocator<U> because they are distinct concrete types with no conversion relationship. The std::uses_allocator mechanism requires the element's allocator_type to be the same as or convertible from the container's allocator type, not merely rebindable to a different value type; since std::string hardcodes std::allocator, rebinding the container's allocator does not change the element's expected allocator type.
What specific lifetime risk arises when using std::pmr::monotonic_buffer_resource with std::pmr::string, and why is this detection harder than with standard allocators?
Because std::pmr::polymorphic_allocator is type-erased and stores a pointer to a base std::pmr::memory_resource, the compiler cannot enforce lifetime constraints at compile time. When a std::pmr::string referencing a stack-based monotonic_buffer_resource is moved or copied to a longer-lived scope, the pointer to the memory resource becomes dangling; unlike std::allocator which typically uses the global heap (always valid), accessing the string after buffer destruction results in use-after-free. Static analyzers struggle to detect this because the virtual do_allocate/do_deallocate interface hides the underlying resource's lifetime from the type system.