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++11An 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
InnerAllocsspecified):inner_allocator()returns*this, so the same adaptor propagates to every depth. - Peeling (one or more
InnerAllocsspecified):inner_allocator()returnsscoped_allocator_adaptor<InnerAllocs...>, consuming one allocator per nesting level; the final inner alloc becomes recursive.
Syntax
#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
#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 arenaRecursive 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:
#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 dataDifferent Allocators per Nesting Level
Provide InnerAllocs... to assign a specific allocator to each depth. The last inner allocator becomes recursive for any further levels:
#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:
#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
#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: falseBest 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:
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:
Thas a nestedallocator_typethat is convertible fromAlloc, or- 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):
// 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):
// 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.
#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 anOuter&β used to allocate the container's own storage.inner_allocator()returnsscoped_allocator_adaptor<Inner1, Inner2>β passed to direct element constructors.- At the next level, that adaptor's
outer_allocator()isInner1, and itsinner_allocator()isscoped_allocator_adaptor<Inner2>. scoped_allocator_adaptor<Inner2>has no further inners, soinner_allocator()returns*thisβInner2becomes 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:
#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_resThree-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:
#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 arenaThis 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
pmrtypes. - You need type-level differentiation β different allocator types for different nesting depths
(the multi-
InnerAllocsform); PMR only supports runtime differentiation viamemory_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_typeas a concrete, non-pmrallocator β the PMR path requires thatallocator_typebepmr::polymorphic_allocatoror 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:
// 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:
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:
// 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
std::uses_allocatorβ the opt-in trait that marks a type as allocator-aware (C++11)std::pmr::polymorphic_allocatorβ runtime-polymorphic allocator with built-inuses_allocatorpropagation (C++17)std::pmr::memory_resourceβ abstract base for custom memory resources (C++17)std::allocator_traitsβ uniform interface over all allocator types; defines defaults for missing members (C++11)std::uses_allocator_construction_argsβ free function that computes the right argument pack for allocator-aware construction (C++20)