C++ / a working model

70 / 163   ·   C++17   ·   10 min

Allocator and pmr: choosing a resource and its lifetime

Keep this sentence

Allocators separate a container's storage-acquisition policy from element management. C++17 pmr lets you choose a memory resource at run time, but the container does not shared-own that resource. The buffer, the resource, and the objects that use them must live and die in the right order.

In this lesson
  1. Obtaining storage and constructing objects are two steps
  2. A monotonic resource fits a batch that is released as a whole
  3. Destruction order and allocator propagation
  4. Example
  5. Exercise

Obtaining storage and constructing objects are two steps

A container obtains storage that meets size and alignment requirements through an allocator and allocator_traits, then constructs and destroys elements in that storage. Changing allocator does not turn vector elements into nodes and does not change its iterator-invalidation rules. A custom allocation policy fits cases where allocation cost or lifetime pattern is already confirmed. It is not a general optimization to apply before measuring.

A traditional allocator type is part of the container type. C++17 pmr::polymorphic_allocator<T> chooses a policy through a run-time memory_resource interface, so one pmr container type can use different resources. A resource pointer does not mean shared ownership. The container does not automatically extend the resource's lifetime.

A monotonic resource fits a batch that is released as a whole

monotonic_buffer_resource allocates from an existing buffer and, when that space is exhausted, asks an upstream resource for more storage. A single deallocate does not recycle space. Upstream memory is released as a whole on release or when the resource is destroyed. That fits a batch of objects from one request, one parse, or one build round. It does not fit a long-lived container that keeps erasing and inserting while expecting immediate reuse.

Reserving a vector's final size matters especially here: old storage left by growth is not reused immediately, so cumulative consumption can exceed the final capacity. Growth factor and memory layout are implementation details. If falling back to the heap must never happen, choose null_memory_resource as upstream explicitly and handle allocation failure. Providing a stack buffer alone does not let you claim the program allocates no heap memory.

Destruction order and allocator propagation

Define the buffer first, then the resource, then construct the container, so reverse-order destruction destroys the container first. release does not call element destructors. Even after clear has erased every element, a vector may still hold capacity the resource provided, so the safe sequence is to destroy every user and only then release. Do not return a pmr container or string view that refers to a local resource.

The example uses pmr::vector<pmr::string> so both the container storage and the strings' internal storage use that resource. Ordinary std::string does not automatically switch to it. Copying into an ordinary string before leaving the scope yields an independent result. pmr allocators do not propagate on container assignment and swap. swap of unequal resources is not valid. Move assignment across resources may also move element by element rather than taking over the buffer.

Pitfalls

  • Releasing a resource is not object destruction. Calling release and then letting a container that still holds that storage operate or destroy violates lifetime and resource-use requirements.
  • Matching pmr container types does not mean equal allocators, and it does not guarantee a zero-cost move. swap between different resource instances must check resource equivalence.

Run an example

Minimum C++17 · complete program · Download .cpp

#include <array>
#include <cassert>
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>

std::string build_name() {
    alignas(std::max_align_t) std::array<std::byte, 1024> buffer{};
    std::pmr::monotonic_buffer_resource arena(buffer.data(), buffer.size());
    std::string result;
    {
        std::pmr::vector<std::pmr::string> names{&arena};
        names.reserve(2);
        names.emplace_back("alpha");
        names.emplace_back("beta");
        assert(names[0] == "alpha" && names[1] == "beta");
        assert(names[0].get_allocator().resource() == &arena);
        result.assign(names[0].data(), names[0].size());
        result += ':';
        result.append(names[1].data(), names[1].size());
    }
    arena.release();
    return result;
}

int main() {
    std::string name = build_name();
    assert(name == "alpha:beta");
    std::cout << name << '\n';
}

Compile locally

g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread stl-allocators.cpp -o example && ./example

Expected result

alpha:beta

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

A function creates a monotonic_buffer_resource internally and returns a pmr::vector<int> bound to that resource. Even if the return is moved or copy-elided, why is that still unsafe? Give two workable designs.

Show a reference answer

Move or copy elision only changes how the container object is constructed. It does not extend the local resource's lifetime. After return the resource is already destroyed, so element storage or the allocator's resource pointer is no longer usable. Have the caller create the resource and pass it in, covering the returned container's whole lifetime; or copy the result into an ordinary vector<int> that uses independent storage before the function returns.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog