There are quite a few questions on here which talk about uses allocator and scoped allocator but they come from an assumption of knowing what these are and what they are for.
The explanation in the usual C references explains the what but not so much the how or why.
This site is missing a canonical answer that will provide the background for those unfamiliar with the topic.
CodePudding user response:
It is a protocol that allows generic container types (in the broader sense of the word) to detect when generic type parameters expect to be using with allocators.
They can then decide to support that by automatically propagating their outside allocators to the inner types. The quintessential example would be when you want to have a std::map<std::vector<std::string>, std::set<int> >
all using the same allocator family.
Types containing a nested allocator_type
typename will automatically be detected as uses_allocator
types, which includes all standard containers.
Typical other uses_allocator
types in the standard library are std::pair
and std::tuple
, but also function wrapper types (std::function
, std::packaged_type
) and container adaptors (std::stack
and friends).
Scoped allocators allow adapting an outer allocator or an outer allocator and set of inner allocators if they are not identical, informing a scoped-allocator-aware container of the allocator types to be used for inner "uses-allocator" types.
From the boost documentation for scoped_allocator_adaptor
:
This class is a C 03-compatible implementation of
std::scoped_allocator_adaptor
. The class templatescoped_allocator_adaptor
is an allocator template that specifies the memory resource (the outer allocator) to be used by a container (as any other allocator does) and also specifies an inner allocator resource to be passed to the constructor of every element within the container.This adaptor is instantiated with one outer and zero or more inner allocator types. If instantiated with only one allocator type, the inner allocator becomes the
scoped_allocator_adaptor
itself, thus using the same allocator resource for the container and every element within the container and, if the elements themselves are containers, each of their elements recursively.If instantiated with more than one allocator, the first allocator is the outer allocator for use by the container, the second allocator is passed to the constructors of the container's elements, and, if the elements themselves are containers, the third allocator is passed to the elements' elements, and so on. If containers are nested to a depth greater than the number of allocators, the last allocator is used repeatedly, as in the single-allocator case, for any remaining recursions.
In my experience, Boost Container has always supported this protocol better than most standard library implementations, but I admit I haven't checked up on the state of those in years.
I've just provided you with an example of using scoped_allocator_adaptor
in this answer: Does boost interprocess support sharing objects containing pointers between processes?. In that picture, Shared::Bar
is an example of an inner "uses-allocator" type, and it informs the container (vector
) to pass a (converted) copy of its allocator along when constructing elements.
I replicate that answer's code listing here, to make sure the example reference doesn't get out of date:
#include <boost/container/scoped_allocator.hpp>
#include <boost/container/string.hpp>
#include <boost/container/vector.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>
#include <boost/interprocess/managed_shared_memory.hpp>
#include <string>
namespace bip = boost::interprocess;
namespace Shared {
#ifdef COLIRU
using Segment = bip::managed_mapped_file;
#else
using Segment = bip::managed_shared_memory;
#endif
using Mgr = Segment::segment_manager;
template <typename T>
using Alloc = boost::container::scoped_allocator_adaptor< //
bip::allocator<T, Mgr>>;
template <typename T> using Vector = std::vector<T, Alloc<T>>;
using String = boost::container::basic_string< //
char, std::char_traits<char>, Alloc<char>>;
struct Bar {
using allocator_type = Alloc<char>;
String first_name, last_name;
template <typename Alloc>
Bar(std::allocator_arg_t, Alloc alloc) : first_name(alloc)
, last_name(alloc) {}
template <typename Alloc>
Bar(Alloc&& alloc) : first_name(alloc)
, last_name(alloc) {}
Bar(Bar const&) = default;
template <typename Alloc>
Bar(Bar const& rhs, Alloc alloc) : first_name(rhs.first_name, alloc), last_name(rhs.last_name, alloc) {}
Bar& operator=(Bar const&) = default;
template <typename Alloc> Bar(std::string_view first_name, std::string_view last_name, Alloc alloc) :
first_name(first_name, alloc), last_name(last_name, alloc) {}
};
struct Snafu {
std::array<int, 5> no_problem;
};
struct Foo {
Vector<Bar> bars;
Vector<Snafu> snafus;
template <typename Alloc> //
Foo(Alloc alloc) : bars(alloc)
, snafus(alloc) {}
};
}
#include <iostream>
static inline std::ostream& operator<<(std::ostream& os, Shared::Foo const& foo) {
os << "Foo\n================\nBars:\n";
for (auto& [f, l] : foo.bars)
os << " - " << f << ", " << l << "\n";
os << "Snafus:";
for (auto& s : foo.snafus) {
os << "\n - ";
for (auto el : s.no_problem)
os << " " << el;
}
return os << "\n";
}
int main() { Shared::Segment msm(bip::open_or_create, "my_shared_mem", 10ull << 10);
Shared::Foo& foo = *msm.find_or_construct<Shared::Foo>("the_foo") //
(msm.get_segment_manager()); // constructor arguments
foo.bars.emplace_back("John", "Doe");
foo.bars.emplace_back("Jane", "Deer");
foo.bars.emplace_back("Igor", "Stravinsky");
foo.bars.emplace_back("Rimsky", "Korsakov");
foo.snafus.push_back({1, 2, 3});
foo.snafus.push_back({2, 3, 4});
foo.snafus.push_back({3, 4, 5, 6, 7});
std::cout << foo << "\n";
}