En el C++ estándar, el tipo de un contenedor incluye el tipo de su Allocator como parámetro de plantilla. Esto genera un problema de “explosión de tipos” (template bloat): un std::vector<int, std::allocator<int>> y un std::vector<int, MyCustomAlloc<int>> son tipos completamente distintos para el compilador, lo que impide que una función acepte ambos sin recurrir a plantillas o interfaces complejas. std::pmr [C++17] (Polymorphic Memory Resources) resuelve esto mediante una capa de abstracción basada en el polimorfismo de ejecución.
En lugar de que el tipo del contenedor dependa de la estrategia de asignación, utiliza un std::pmr::polymorphic_allocator<T>, que es un objeto ligero que contiene un puntero a una clase base abstracta: std::pmr::memory_resource. Esto permite que todos los contenedores de la familia std::pmr tengan el mismo tipo, pero se comporten de formas radicalmente distintas según el memory_resource que se les pase en tiempo de ejecución. El mecanismo interno se basa en una tabla virtual (vtable) que redirige las llamadas de allocate y deallocate al recurso específico.
Debes usar std::pmr cuando necesites cambiar la estrategia de asignación sin alterar las firmas de tus funciones, o cuando busques optimizaciones de rendimiento críticas. Por ejemplo, si tienes un bucle de alta frecuencia que realiza muchas pequeñas asignaciones temporales, un std::pmr::monotonic_buffer_resource es ideal porque simplemente desplaza un puntero de memoria (“bump allocation”) sin el coste de buscar huecos en el heap o llamar al sistema operativo, liberando todo el bloque de golpe al destruir el recurso. Si tu problema es la fragmentación de memoria por objetos de tamaños similares, un std::pmr::pool_resource es la opción adecuada.
Sin embargo, si te equivocas en el ciclo de vida de los objetos, el error es fatal. El error más común es que el memory_resource sea destruido antes que los contenedores que lo utilizan, dejando a los contenedores con un puntero colgante (dangling pointer) hacia una estructura de control de memoria inexistente.
#include <iostream>
#include <vector>
#include <string>
#include <memory_resource>
#include <array>
#include <cstddef>
// Esta función es agnóstica a la estrategia de memoria.
// Puede recibir un vector con memoria en el stack, en un pool, o en el heap.
void procesar_datos(std::pmr::vector<std::pmr::string>& datos) {
for (auto& s : datos) {
s += "_procesado"; // La modificación ocurre en el recurso asignado.
}
}
int main() {
// 1. Buffer en el stack para evitar syscalls de malloc/new.
std::array<std::byte, 1024> stack_buffer;
// 2. Recurso monotónico: asignación ultra rápida, no desasigna individualmente.
// Actúa como el "upstream" para el siguiente recurso.
std::pmr::monotonic_buffer_resource mono_res{stack_buffer.data(), stack_buffer.size()};
// 3. Pool de memoria: optimizado para evitar fragmentación de objetos pequeños.
// Se alimenta de mono_res, por lo que las nuevas páginas de memoria vendrán de stack_buffer.
std::pmr::unsynchronized_pool_resource pool_res{&mono_res};
// 4. Contenedor polimórfico usando el pool.
std::pmr::vector<std::pmr::string> vec{&pool_res};
// IMPORTANTE: Para que la string use el mismo pool, debemos pasarle el recurso.
// Si no se pasa, string usará el recurso global por defecto (el heap).
vec.emplace_back(pool_res, "item_1");
vec.emplace_back(pool_res, "item_2");
vec.emplace_back(pool_res, "item_3");
procesar_datos(vec);
for (const auto& s : vec) {
std::cout << s << "\n";
}
return 0;
}
Para entender qué ocurre debajo del capó, fíjate en la jerarquía de asignación del ejemplo. Cuando vec.emplace_back es llamado, el std::pmr::vector solicita memoria a pool_res. Este recurso, al ser un unsynchronized_pool_resource, gestiona bloques de memoria para tamaños específicos. Si el pool necesita un bloque nuevo, le pide memoria a mono_res. Como mono_res es un monotonic_buffer_resource, simplemente toma un segmento del stack_buffer y actualiza su puntero interno; esto es extremadamente rápido porque no hay búsqueda de huecos ni llamadas al kernel.
Un detalle crítico en la línea vec.emplace_back(pool_res, "item_1"); es que estamos pasando explícitamente pool_res al constructor de std::pmr::string. Dado que std::pmr::string es un alias de una std::basic_string con un polymorphic_allocator, si no pasamos el recurso, el objeto string utilizará el recurso por defecto (std::get_default_resource()), lo que anularía por completo el beneficio de usar un buffer en el stack para los datos de la cadena.
El error frecuente
El error más sutil y peligroso es la violación de la jerarquía de vida de los recursos. El memory_resource debe vivir obligatoriamente más tiempo que cualquier contenedor que dependa de él.
// ERROR CRÍTICO: Undefined Behavior
std::pmr::vector<int> crear_vector_peligroso() {
std::array<std::byte, 512> buffer;
std::pmr::monotonic_buffer_resource res{buffer.data(), buffer.size()};
std::pmr::vector<int> v{&res};
v.push_back(42);
return v; // ¡PELIGRO! 'res' y 'buffer' mueren aquí, 'v' queda invalidado.
}
En este escenario, el objeto std::pmr::vector devuelto contiene un puntero a res, pero res es una variable local que se destruye al salir de la función. Al intentar usar el vector en el main, el programa intentará acceder a memoria invalidada o a la vtable de un objeto destruido. Herramientas como AddressSanitizer (ASan) detectarán este error como un stack-use-after-scope o un use-after-destruction al ejecutar el binario compilado con -fsanitize=address.
N° 115