Skip to content
C++
Library
since C++11
Advanced

std::scoped_allocator_adaptor

C++11 allocator adaptor that propagates one or more allocators through every nesting level of a container hierarchy via std::uses_allocator.

std::scoped_allocator_adaptorsince C++11

An allocator adaptor, from <scoped_allocator>, that intercepts element construction inside a container and automatically passes an inner allocator to any allocator-aware element type, propagating a single allocator (or a per-level chain) through every depth of a nested container hierarchy.

Overview

A custom allocator only governs the storage of the container it is directly given. Without scoped_allocator_adaptor, using MyAlloc with vector<vector<int, MyAlloc>, MyAlloc> allocates the outer buffer with MyAlloc, but the inner vectors are default-constructed β€” their storage comes from the global heap, entirely bypassing MyAlloc.

scoped_allocator_adaptor fixes this by overriding construct(). When constructing an element of type T, it checks std::uses_allocator<T, inner_allocator_type> (C++11). If true, it injects the inner allocator into the element's constructor using one of two standard protocols:

  • Leading-allocator: T(std::allocator_arg, alloc, args...)
  • Trailing-allocator: T(args..., alloc)

The inner allocator itself is:

  • Recursive (no InnerAllocs specified): inner_allocator() returns *this, so the same adaptor propagates to every depth.
  • Peeling (one or more InnerAllocs specified): inner_allocator() returns scoped_allocator_adaptor<InnerAllocs...>, consuming one allocator per nesting level; the final inner alloc becomes recursive.

Syntax

cpp
#include <scoped_allocator>  // C++11

namespace std {
    template <class OuterAlloc, class... InnerAllocs>
    class scoped_allocator_adaptor : public OuterAlloc {
    public:
        using outer_allocator_type = OuterAlloc;
        // inner_allocator_type:
        //   sizeof...(InnerAllocs) == 0  β†’  scoped_allocator_adaptor<OuterAlloc>   (recursive: inner == *this)
        //   sizeof...(InnerAllocs) >= 1  β†’  scoped_allocator_adaptor<InnerAllocs...>
        using inner_allocator_type = /* see above */;
        using value_type           = typename OuterAlloc::value_type;

        scoped_allocator_adaptor();
        template <class OA2>
        scoped_allocator_adaptor(OA2&& outer, const InnerAllocs&... inner);

        outer_allocator_type&       outer_allocator()       noexcept;
        const outer_allocator_type& outer_allocator() const noexcept;
        inner_allocator_type&       inner_allocator()       noexcept;
        const inner_allocator_type& inner_allocator() const noexcept;

        pointer  allocate(size_type n);          // delegates to outer_allocator()
        void     deallocate(pointer p, size_type n);

        template <class T, class... Args>
        void construct(T* p, Args&&... args);    // injects inner_allocator() if uses_allocator<T,...>
    };
}

Examples

The Core Problem and Its Fix

cpp
#include <scoped_allocator>
#include <vector>
#include <memory_resource>  // C++17

std::pmr::monotonic_buffer_resource arena{65536};

// ── Without scoped_allocator_adaptor ──────────────────────────────────────
// The outer vector uses the pmr allocator for its own buffer.
// emplace_back() default-constructs the inner vector β€” it gets std::allocator,
// not the arena. All inner allocations escape to the heap.
using InnerV   = std::vector<int, std::pmr::polymorphic_allocator<int>>;  // C++17
using OuterBad = std::pmr::polymorphic_allocator<InnerV>;                 // C++17
std::vector<InnerV, OuterBad> bad{&arena};
bad.emplace_back();          // inner vector: std::allocator<int> β€” WRONG

// ── With scoped_allocator_adaptor ─────────────────────────────────────────
// construct() detects uses_allocator<InnerV, pmr::polymorphic_allocator<int>>
// and passes the inner allocator to InnerV's constructor.
using ScopedAlloc = std::scoped_allocator_adaptor<
                        std::pmr::polymorphic_allocator<InnerV>>;         // C++11
std::vector<InnerV, ScopedAlloc> good{ScopedAlloc{&arena}};
good.emplace_back();         // inner vector receives &arena β€” all storage in arena
good[0].push_back(1);        // inner ints also from arena

Recursive Mode β€” One Allocator for All Depths

When no InnerAllocs are specified, inner_allocator() returns *this. The same allocator propagates through arbitrarily deep nesting without spelling out each level:

cpp
#include <scoped_allocator>
#include <vector>
#include <string>
#include <memory_resource>  // C++17

std::pmr::unsynchronized_pool_resource pool;  // C++17

// Three levels: vector β†’ vector β†’ string
// Only one allocator type specified; recursion handles the rest.
using Str   = std::pmr::string;                             // C++17
using L1    = std::vector<Str,  std::pmr::polymorphic_allocator<Str>>;   // C++17
using Scoped = std::scoped_allocator_adaptor<
                   std::pmr::polymorphic_allocator<L1>>;    // C++11

std::vector<L1, Scoped> doc{Scoped{&pool}};

doc.emplace_back();                  // L1 constructed with &pool
doc[0].emplace_back("paragraph 1");  // Str constructed with &pool β€” no heap
doc[0].emplace_back("paragraph 2");  // same pool for string data

Different Allocators per Nesting Level

Provide InnerAllocs... to assign a specific allocator to each depth. The last inner allocator becomes recursive for any further levels:

cpp
#include <scoped_allocator>
#include <vector>
#include <string>
#include <memory_resource>  // C++17

// Two separate arenas β€” one for container nodes, one for string character data.
std::byte   node_buf[32768];
std::byte   char_buf[65536];
std::pmr::monotonic_buffer_resource node_arena{node_buf, sizeof(node_buf)};  // C++17
std::pmr::monotonic_buffer_resource char_arena{char_buf, sizeof(char_buf)};  // C++17

using CharAlloc = std::pmr::polymorphic_allocator<char>;          // C++17
using StrType   = std::basic_string<char,
                      std::char_traits<char>, CharAlloc>;
using NodeAlloc = std::pmr::polymorphic_allocator<StrType>;       // C++17

// OuterAlloc: node_arena allocates vector's StrType elements
// InnerAlloc: char_arena allocates character buffers inside each StrType
using TwoLevel = std::scoped_allocator_adaptor<NodeAlloc, CharAlloc>;  // C++11

std::vector<StrType, TwoLevel> words{TwoLevel{&node_arena, &char_arena}};
words.emplace_back("hello");   // StrType node from node_arena, chars from char_arena
words.emplace_back("world");

Custom Allocator-Aware Types

Any type can participate by declaring allocator_type and a suitable constructor:

cpp
#include <scoped_allocator>
#include <vector>
#include <memory_resource>  // C++17

struct LogEntry {
    using allocator_type = std::pmr::polymorphic_allocator<char>;  // C++17

    std::pmr::string message;   // C++17
    std::pmr::vector<int> ids;  // C++17

    // Trailing-allocator constructor (standard protocol)
    LogEntry(std::string_view msg, const allocator_type& alloc = {})
        : message{msg, alloc}, ids{alloc} {}
};

// uses_allocator<LogEntry, pmr::polymorphic_allocator<char>> is true (C++11)
static_assert(
    std::uses_allocator<LogEntry,
        std::pmr::polymorphic_allocator<char>>::value);  // C++11

std::pmr::monotonic_buffer_resource arena{131072};       // C++17
using ScopedAlloc = std::scoped_allocator_adaptor<
                        std::pmr::polymorphic_allocator<LogEntry>>;  // C++11

std::vector<LogEntry, ScopedAlloc> log{ScopedAlloc{&arena}};
log.emplace_back("connection established");  // LogEntry + message + ids: all in arena
log.emplace_back("request received");

Verifying Propagation Eligibility

cpp
#include <memory>
#include <vector>
#include <string>

// std::uses_allocator<T, Alloc>::value  β€” C++11 (::value form)
// std::uses_allocator_v<T, Alloc>       β€” C++17 (variable template shorthand)

static_assert( std::uses_allocator_v<std::vector<int>, std::allocator<int>>);  // C++17
static_assert( std::uses_allocator_v<std::string, std::allocator<char>>);      // C++17
static_assert(!std::uses_allocator_v<int, std::allocator<int>>);               // primitives: false

Best Practices

Prefer the recursive (zero-inner-allocs) form. scoped_allocator_adaptor<MyAlloc<T>> covers the overwhelming majority of use cases β€” the same allocator propagates to every depth without manually enumerating levels.

With C++17 PMR, you often do not need this adaptor. std::pmr::polymorphic_allocator::construct() already implements uses_allocator injection internally. std::pmr::vector<std::pmr::vector<int>> with a shared memory_resource* propagates the resource automatically. Reach for scoped_allocator_adaptor when working with pre-C++17 stateful custom allocators.

Use emplace_back(), not push_back(value). The adaptor's construct() fires during in-place construction. push_back copies or moves an already-constructed object; the copy constructor does not receive the allocator unless the type's propagate_on_container_copy_assignment is set.

Check propagate_on_container_* traits. The adaptor respects propagate_on_container_copy_assignment, propagate_on_container_move_assignment, and propagate_on_container_swap from the outer allocator. If your custom allocator leaves these as false_type (the default), allocators won't transfer on assignment.

Verify rebind consistency. scoped_allocator_adaptor inherits from OuterAlloc and rebound copies must carry the same state. Ensure OuterAlloc::rebind<U>::other preserves the state pointer/reference; a stateless rebind silently severs the connection to your memory resource.

Common Pitfalls

value_type mismatch triggers rebind. scoped_allocator_adaptor<Alloc<Outer>> has value_type == Outer. Attaching it to vector<Inner> with Inner != Outer triggers a rebind. If your allocator's rebind is incomplete or incorrect, you'll get cryptic substitution failures.

Depth semantics for multi-alloc form. With scoped_allocator_adaptor<A, B, C>, level 0 uses A, level 1 uses B, and level 2+ uses C (recursively). It is NOT the case that C applies only to level 2 β€” it becomes the recursive base for all deeper levels. Design allocator chains with this peeling behaviour in mind.

The adaptor does not propagate through raw new or placement new. It only acts via its own construct() member, which is called by the container machinery. If your element type constructs sub-objects via raw allocation internally (without going through an allocator-aware constructor), those allocations remain outside the chain.

Equality semantics. Two scoped_allocator_adaptor instances compare equal only if their outer and all inner allocators compare equal. Getting this wrong causes undefined behaviour on container swap and merge operations.

The Problem in Detail: Why Nested Containers Break Without This Adaptor

Consider this type alias:

cpp
using Row    = std::vector<int, MyAlloc<int>>;
using Matrix = std::vector<Row, MyAlloc<Row>>;

When Matrix grows and calls allocator_traits<MyAlloc<Row>>::construct(alloc, ptr), it default-constructs a Row. The Row constructor that fires is vector<int, MyAlloc<int>>() β€” which itself uses its own default-constructed MyAlloc<int>, completely ignoring the outer alloc. Every inner Row therefore draws memory from wherever MyAlloc<int>() points, which is usually the global heap.

scoped_allocator_adaptor replaces the plain MyAlloc<Row> as the outer container's allocator. Its construct() override detects that Row is allocator-aware via std::uses_allocator, then calls allocator_traits::construct with the inner allocator appended to the argument list. The inner Row is born already holding your custom allocator.

The uses_allocator Protocol

std::uses_allocator<T, Alloc> (defined in <memory>, C++11) is the trait that answers: "does type T know how to accept an allocator of type Alloc?" It yields true when either:

  1. T has a nested allocator_type that is convertible from Alloc, or
  2. Specialization explicitly sets value = true (used by some third-party types).

When the trait is true, allocator_traits::construct and scoped_allocator_adaptor::construct choose one of two injection protocols, checked in this order:

Leading-allocator convention (used by std::tuple and a few older types):

cpp
// T must be constructible as:
T(std::allocator_arg_t, const Alloc&, Args&&...)

Trailing-allocator convention (used by all standard containers and std::pmr types):

cpp
// T must be constructible as:
T(Args&&..., const Alloc&)

If neither constructor exists but uses_allocator<T, Alloc> is true, the program is ill-formed. If uses_allocator<T, Alloc> is false, the element is constructed without any allocator argument β€” exactly as if you had written ::new(ptr) T(args...).

C++20 adds std::uses_allocator_construction_args<T>(alloc, args...), a free function that returns the correctly ordered argument tuple for either convention, making it easy to implement the protocol in your own factory code without reimplementing the detection logic.

cpp
#include <memory>
#include <vector>
#include <string>

// Checking uses_allocator for standard types:
static_assert(std::uses_allocator_v<std::vector<int>,
                  std::allocator<int>>);       // true β€” vector has allocator_type
static_assert(std::uses_allocator_v<std::string,
                  std::allocator<char>>);      // true
static_assert(!std::uses_allocator_v<int,
                  std::allocator<int>>);       // false β€” primitives: no allocator_type

// C++20: let the standard compute the right arg order for you
auto args = std::uses_allocator_construction_args<std::pmr::string>(
                std::pmr::polymorphic_allocator<char>{},
                "hello");
// args == tuple{ "hello", pmr::polymorphic_allocator<char>{} }
// (trailing form β€” pmr::string uses trailing convention)

scoped_allocator_adaptor with Multiple Allocator Types

When you specify more than one allocator type β€” scoped_allocator_adaptor<Outer, Inner1, Inner2> β€” the adaptor peels one layer per nesting level:

  • outer_allocator() returns an Outer& β€” used to allocate the container's own storage.
  • inner_allocator() returns scoped_allocator_adaptor<Inner1, Inner2> β€” passed to direct element constructors.
  • At the next level, that adaptor's outer_allocator() is Inner1, and its inner_allocator() is scoped_allocator_adaptor<Inner2>.
  • scoped_allocator_adaptor<Inner2> has no further inners, so inner_allocator() returns *this β€” Inner2 becomes recursive for any remaining depth.

This gives you type-level control over which allocator each depth receives. A practical use case is separating node memory from character buffer memory in a vector of strings:

cpp
#include <scoped_allocator>
#include <vector>
#include <string>
#include <memory_resource>

std::byte node_buf[32768], char_buf[131072];
std::pmr::monotonic_buffer_resource
    node_res{node_buf, sizeof(node_buf)},
    char_res{char_buf, sizeof(char_buf)};

// Outer alloc (node_res) allocates the vector's array of pmr::string objects.
// Inner alloc (char_res) is passed to each pmr::string constructor for its character buffer.
using TwoLevel = std::scoped_allocator_adaptor<
    std::pmr::polymorphic_allocator<std::pmr::string>,   // outer
    std::pmr::polymorphic_allocator<char>                // inner
>;

std::vector<std::pmr::string, TwoLevel> words{
    TwoLevel{&node_res, &char_res}
};

words.emplace_back("hello");   // string object in node_res, chars in char_res
words.emplace_back("world");
words.emplace_back(50, 'x');   // 50-char string β€” all character data in char_res

Three-level nesting with distinct allocators at each depth follows the same pattern: just add a third template argument. The last argument always becomes the recursive base for any further levels beyond what you explicitly named.

PMR vs. scoped_allocator_adaptor: Choosing the Right Tool

std::pmr::polymorphic_allocator (C++17) implements uses_allocator propagation directly inside its own construct() member, so a pmr container already does what scoped_allocator_adaptor was designed for β€” without an extra wrapper:

cpp
#include <vector>
#include <string>
#include <memory_resource>

std::pmr::monotonic_buffer_resource arena{65536};
std::pmr::polymorphic_allocator<std::pmr::string> alloc{&arena};

// pmr::vector's allocator is polymorphic_allocator, which propagates to element ctors.
// No scoped_allocator_adaptor needed.
std::pmr::vector<std::pmr::string> v{alloc};
v.emplace_back("no adaptor required");   // string's chars also from arena

This works because pmr::polymorphic_allocator::construct() unconditionally injects the memory_resource* into any allocator-aware element type via the uses_allocator protocol.

Use scoped_allocator_adaptor instead when:

  • You are working with pre-C++17 stateful allocators that do not use pmr types.
  • You need type-level differentiation β€” different allocator types for different nesting depths (the multi-InnerAllocs form); PMR only supports runtime differentiation via memory_resource*.
  • You are wrapping a non-PMR third-party allocator (e.g., a pool library that predates C++17) and want it to propagate into nested standard containers.
  • Your element types advertise allocator_type as a concrete, non-pmr allocator β€” the PMR path requires that allocator_type be pmr::polymorphic_allocator or convertible from it.

When PMR is available and your element types all use pmr allocators, prefer the simpler PMR approach. Reach for scoped_allocator_adaptor when you need the extra control it provides.

Propagation Traits and Copy/Move/Swap Behaviour

scoped_allocator_adaptor delegates all propagation trait queries to the outer allocator:

cpp
// All four traits are inherited from OuterAlloc:
propagate_on_container_copy_assignment  // should allocator transfer on copy-assign?
propagate_on_container_move_assignment  // should allocator transfer on move-assign?
propagate_on_container_swap             // should allocators swap with the container?
select_on_container_copy_construction   // which allocator should a copy-constructed container use?

select_on_container_copy_construction is called when a container is copy-constructed. By default (if OuterAlloc does not define it), allocator_traits returns a default-constructed copy of the allocator. For arena-backed allocators this is almost never correct β€” you want copies to share the arena, not get a fresh default allocator. Override this in your outer allocator to return the same instance:

cpp
struct MyArenaAlloc {
    using value_type = /* ... */;
    using propagate_on_container_copy_assignment = std::true_type;
    using propagate_on_container_move_assignment = std::true_type;
    using propagate_on_container_swap            = std::true_type;

    MyArenaAlloc select_on_container_copy_construction() const {
        return *this;   // copies share the same arena
    }
};

When propagate_on_container_copy_assignment is false_type (the default), assigning one container to another does not replace the destination's allocator β€” elements are copied using the destination's existing allocator. This is correct if your allocator is tied to a local buffer that must outlive the container, but it means the copy and source may hold memory from different pools.

propagate_on_container_swap deserves special attention: if it is false_type and two containers with unequal allocators are swapped, the behaviour is undefined. Prefer true_type for any stateful allocator used inside scoped_allocator_adaptor to avoid this trap.

The adaptor forwards these same trait values outward so that any container holding a scoped_allocator_adaptor sees the right answers when its own copy/move/swap machinery queries allocator_traits.

Under the Hood: What construct() Actually Does

The adaptor's construct(T* p, Args&&... args) member follows this logic at compile time:

cpp
// Pseudocode illustrating the dispatch inside scoped_allocator_adaptor::construct():

template <class T, class... Args>
void construct(T* p, Args&&... args) {
    using IA = inner_allocator_type;

    if constexpr (std::uses_allocator_v<T, IA>) {
        // T is allocator-aware β€” inject inner_allocator()
        if constexpr (std::is_constructible_v<T,
                          std::allocator_arg_t, IA, Args...>) {
            // Leading convention
            std::allocator_traits<OuterAlloc>::construct(
                outer_allocator(), p,
                std::allocator_arg, inner_allocator(),
                std::forward<Args>(args)...);
        } else {
            // Trailing convention
            std::allocator_traits<OuterAlloc>::construct(
                outer_allocator(), p,
                std::forward<Args>(args)...,
                inner_allocator());
        }
    } else {
        // T is not allocator-aware β€” construct normally
        std::allocator_traits<OuterAlloc>::construct(
            outer_allocator(), p,
            std::forward<Args>(args)...);
    }
}

Note that construct() for the final storage still delegates to allocator_traits<OuterAlloc>::construct, preserving any custom construction logic in your outer allocator (such as placement-new wrappers for debugging or sanitization). The adaptor sits above the outer allocator, not beside it.

See Also