std::pmr::memory_resource: гибкое управление памятью в C++17

Автор: | 13 июня, 2025

Стандарт C++17 представил новый механизм управления памятью через пространство имен std::pmr (Polymorphic Memory Resources). Центральным элементом этой системы является абстрактный базовый класс std::pmr::memory_resource, который предоставляет полиморфный интерфейс для выделения и освобождения памяти.

Этот механизм позволяет:

  • Легко настраивать стратегии управления памятью
  • Использовать разные аллокаторы для разных частей программы
  • Создавать каскадные системы выделения памяти
  • Реализовывать специализированные менеджеры памяти

Базовый интерфейс memory_resource

namespace std::pmr {
    class memory_resource {
    public:
        virtual ~memory_resource() = default;
        
        void* allocate(size_t bytes, size_t alignment = alignof(max_align_t));
        void deallocate(void* p, size_t bytes, size_t alignment = alignof(max_align_t));
        
        bool is_equal(const memory_resource& other) const noexcept;
        
    protected:
        virtual void* do_allocate(size_t bytes, size_t alignment) = 0;
        virtual void do_deallocate(void* p, size_t bytes, size_t alignment) = 0;
        virtual bool do_is_equal(const memory_resource& other) const noexcept = 0;
    };
}

Стандартные реализации memory_resource

C++17 предоставляет несколько готовых реализаций memory_resource:

new_delete_resource

Использует глобальные операторы new и delete (аналог std::allocator).

std::pmr::memory_resource* res = std::pmr::new_delete_resource();

null_memory_resource

Генерирует исключение std::bad_alloc при любой попытке выделения памяти.

std::pmr::memory_resource* res = std::pmr::null_memory_resource();

synchronized_pool_resource

Потокобезопасный аллокатор на основе пулов памяти.

unsynchronized_pool_resource

Несинхронизированный (для использования в одном потоке) аллокатор на основе пулов.

monotonic_buffer_resource

Быстрый, но не возвращающий память аллокатор, выделяющий память из предоставленного буфера.

Пример использования monotonic_buffer_resource

#include <memory_resource>
#include <vector>
#include <iostream>

int main() {
    // Буфер на стеке для аллокатора
    char buffer[1024];
    
    // Создаем monotonic_buffer_resource, использующий наш буфер
    std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
    
    // Создаем вектор, использующий наш memory_resource
    std::pmr::vector<int> vec{&pool};
    
    for (int i = 0; i < 10; ++i) {
        vec.push_back(i);
        std::cout << "Использовано памяти: " << pool.bytes_allocated() 
                  << " из " << pool.bytes_available() << "\n";
    }
    
    // При выходе из области видимости память автоматически освободится
}

Создание собственного memory_resource

Вы можете создать собственный аллокатор, унаследовавшись от memory_resource:

class CustomMemoryResource : public std::pmr::memory_resource {
protected:
    void* do_allocate(size_t bytes, size_t alignment) override {
        std::cout << "Выделение " << bytes << " байт\n";
        return ::operator new(bytes, std::align_val_t{alignment});
    }
    
    void do_deallocate(void* p, size_t bytes, size_t alignment) override {
        std::cout << "Освобождение " << bytes << " байт\n";
        ::operator delete(p, bytes, std::align_val_t{alignment});
    }
    
    bool do_is_equal(const memory_resource& other) const noexcept override {
        return this == &other;
    }
};

int main() {
    CustomMemoryResource custom_res;
    std::pmr::vector<int> vec(&custom_res);
    vec.push_back(42);
}

Цепочки memory_resource

Одна из мощных возможностей — создание цепочек memory_resource:

#include <memory_resource>

int main() {
    // Первичный аллокатор (монотонный буфер)
    char buffer[4096];
    std::pmr::monotonic_buffer_resource primary_pool{std::data(buffer), std::size(buffer)};
    
    // Вторичный аллокатор (пул) использует первичный, когда ему нужно больше памяти
    std::pmr::unsynchronized_pool_resource pool_alloc{&primary_pool};
    
    // Контейнер использует пул-аллокатор
    std::pmr::vector<int> vec{&pool_alloc};
    
    for (int i = 0; i < 1000; ++i) {
        vec.push_back(i);
    }
    
    // Память сначала берется из buffer, затем (если нужно) из кучи через primary_pool
}

Преимущества std::pmr::memory_resource

  1. Полиморфизм: Единый интерфейс для разных стратегий аллокации
  2. Гибкость: Легко комбинировать разные аллокаторы
  3. Производительность: Специализированные аллокаторы могут быть значительно быстрее
  4. Контроль: Точное управление распределением памяти
  5. Совместимость: Работает со всеми pmr-контейнерами (std::pmr::vector, std::pmr::string и др.)

Недостатки и ограничения

  1. Сложность: Более сложный API по сравнению с std::allocator
  2. Накладные расходы: Виртуальные вызовы могут снижать производительность в некоторых случаях
  3. Только C++17 и новее: Не доступен в более старых стандартах

Практические примеры использования

Выделение памяти из заранее подготовленного буфера

void processData() {
    std::byte buffer[1'000'000];
    std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};
    
    std::pmr::vector<std::pmr::string> strings{&pool};
    strings.reserve(100);
    
    // Все строки и вектор используют наш буфер
    for (int i = 0; i < 100; ++i) {
        strings.emplace_back("Пример строки");
    }
    
    // При выходе из функции вся память автоматически "освобождается"
}

Пул аллокаторов для объектов одного типа

struct GameObject {
    // данные объекта...
};

void gameLoop() {
    std::pmr::unsynchronized_pool_resource game_object_pool;
    
    std::pmr::vector<GameObject*> objects;
    
    for (int i = 0; i < 1000; ++i) {
        auto obj = new (game_object_pool.allocate(sizeof(GameObject))) GameObject();
        objects.push_back(obj);
    }
    
    // ... игровая логика ...
    
    // Освобождение всех объектов
    for (auto obj : objects) {
        obj->~GameObject();
        game_object_pool.deallocate(obj, sizeof(GameObject));
    }
}

Заключение

std::pmr::memory_resource представляет собой мощный инструмент для управления памятью в современных программах на C++. Он особенно полезен в сценариях, где:

  • Требуется высокая производительность при работе с памятью
  • Необходим контроль над стратегиями выделения памяти
  • Важно минимизировать фрагментацию памяти
  • Работа ведется с большим количеством мелких объектов

Использование memory_resource позволяет создавать специализированные системы управления памятью, сохраняя при этом совместимость со стандартными контейнерами C++.