Gestión de memoria mediante asignadores y allocator_traits en C++

La clase std::allocator actúa como la estrategia de gestión de memoria predeterminada en la biblioteca estándar de C++. Cuando un contenedor no especifica un asignador diferente, utiliza esta plantilla para gestionar la reserva y liberación de memoria bruta. A diferencia de new, el asignador separa el paso de reserva de memoria de la construcción del objeto.

#include <memory>
#include <iostream>

int main() {
    std::allocator<float> memory_provider;
    
    // Reserva de espacio para 3 elementos tipo float
    float* buffer = memory_provider.allocate(3);
    
    buffer[0] = 1.5f;
    buffer[1] = 2.5f;
    buffer[2] = 3.5f;

    for(int i = 0; i < 3; ++i) {
        std::cout << "Valor " << i << ": " << buffer[i] << std::endl;
    }

    // Liberación del espacio reservado
    memory_provider.deallocate(buffer, 3);
    return 0;
}

En este fragmento, se instancia un asignador para tipos float. Los métodos principales son allocate (para obtener memoria sin inicializar) y deallocate (para liberar dicha memoria). Es importante notar que a partir de C++17, las funciones de construcción y destrucción directa en std::allocator fueron marcadas como obsoletas, delegando esa responsabilidad a la interfaz std::allocator_traits.

El papel de std::allocator_traits

Para estandarizar el acceso a diferentes tipos de asignadores (ya sean estándar o personalizados), C++ proporciona std::allocator_traits. Esta clase actúa como un envoltorio que proporciona implementaciones por defecto incluso si el asignador específico no cuenta con ciertos métodos.

struct Point {
    int x, y;
    Point(int a, int b) : x(a), y(b) { std::cout << "Punto creado\n"; }
    ~Point() { std::cout << "Punto destruido\n"; }
};

std::allocator<Point> base_alloc;
using traits_t = std::allocator_traits<std::allocator<Point>>;

Point* ptr = traits_t::allocate(base_alloc, 1);
traits_t::construct(base_alloc, ptr, 10, 20); // Invoca al constructor

std::cout << "Coordenadas: " << ptr->x << ", " << ptr->y << std::endl;

traits_t::destroy(base_alloc, ptr);           // Invoca al destructor
traits_t::deallocate(base_alloc, ptr, 1);     // Libera memoria

El uso de allocator_traits garantiza que el código sea compatible con cualquier asignador que cumpla con los requisitos mínimos del estándar, abstrayendo detalles como la construcción interna o la definición de tipos auxiliares como pointer o size_type.

Asignadores personalizados

Es posible definir mecanismos de gestión de memoria propios. Al utilizar allocator_traits, solo es necesario implemantar la lógica de reserva y liberación de memoria; el resto de las funcionalidades se completan automáticamente.

template <typename T>
struct FastAlloc : public std::allocator<T> {
    // Se hereda la lógica base, pero se podría sobrescribir allocate/deallocate
};

FastAlloc<int> my_alloc;
std::allocator_traits<FastAlloc<int>>::allocate(my_alloc, 5);

El mecanismo de Rebind en contenedores

Los contenedores complejos, como las listas enlazadas, no suelen asignar memoria para el tipo de dato puro (por ejemplo, int), sino para una estructura interna de nodo que contiene el dato y punteros de control. Para resolver esto, los asignadores utilizan una técnica llamada rebind, que permite transformar un asignador de tipo T en un asignador de tipo U manteneindo la misma estrategia de memoria.

template <typename T, typename Alloc = std::allocator<T>>
class SimpleChain {
    struct Element {
        T value;
        Element* next;
    };

    // Obtenemos un asignador para la estructura interna Element basado en Alloc
    using ElementAlloc = typename std::allocator_traits<Alloc>::template rebind_alloc<Element>;
    using ElementTraits = std::allocator_traits<ElementAlloc>;

    ElementAlloc internal_alloc;
    Element* head = nullptr;

public:
    void add(T val) {
        Element* new_node = ElementTraits::allocate(internal_alloc, 1);
        ElementTraits::construct(internal_alloc, new_node);
        new_node->value = val;
        new_node->next = head;
        head = new_node;
    }

    T get_head_value() { return head->value; }
};

// Uso del contenedor personalizado
SimpleChain<int> list;
list.add(500);
std::cout << "Valor en cabeza: " << list.get_head_value() << std::endl;

En este ejemplo de lista, aunque el usuario provee un asignador para int, el contenedor internamente utiliza rebind_alloc para generar un asignador capaz de manejar objetos Element. Esto asegura que la política de memoria definida por el usuario se aplique de forma consistente a toda la estructura interna del contenedor.

Etiquetas: cpp memory-management STL allocators templates

Publicado el 7-28 15:51