Shooting for the stars, are we :) Painless allocator propagation is the holy grail.
It looks like the Shared:Scoped allocator adapter is key to propagating the allocator from a top level container to its children.
Indeed
I'm not sure if this is different when applied to the boost containers vs the standard containers.
In my understanding, modern C++ standard libraries should support the same, but in practice my experience has shown that it often worked with Boost Container containers. (YMMV and standard library implementations may/will catch up)
What To Do
I think you will want to understand the uses_allocator
protocol: https://en.cppreference.com/w/cpp/memory/uses_allocator

This really answers all of your questions, I suppose. I'll try to come up with a quick sample if I can.
Demo
So far I have got the following two approaches working:
struct MyStruct {
String data;
using allocator_type = Alloc<char>;
MyStruct(MyStruct const& rhs, allocator_type = {}) : data(rhs.data) {}
template <typename I, typename = std::enable_if_t<not std::is_same_v<MyStruct, I>, void> >
MyStruct(I&& init, allocator_type a)
: data(std::forward<I>(init), a)
{ }
};
This allows:
Shared::Segment mf(bip::open_or_create, "test.bin", 10<<20);
auto& db = *mf.find_or_construct<Shared::Database>("db")(mf.get_segment_manager());
db.emplace_back("one");
db.emplace_back("two");
db.emplace_back("three");
The slightly more complicated/versatile (?) approach also works:
MyStruct(std::allocator_arg_t, allocator_type, MyStruct const& rhs) : data(rhs.data) {}
template <
typename I,
typename A = Alloc<char>,
typename = std::enable_if_t<not std::is_same_v<MyStruct, I>, void> >
MyStruct(std::allocator_arg_t, A alloc, I&& init)
: data(std::forward<I>(init), alloc.get_segment_manager())
{ }
It appears that for the current use-case, the inner typedef allocator_type
is enough to signal that MyStruct
supports allocator-construction, making the specialization of uses_allocator<MyStruct, ...>
redundant.
Full Listing
Live On Coliru
#include <boost/interprocess/containers/vector.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/container/scoped_allocator.hpp>
#include <iostream>
namespace bip = boost::interprocess;
namespace Shared {
using Segment = bip::managed_mapped_file;
using SMgr = Segment::segment_manager;
template <typename T> using Alloc = boost::container::scoped_allocator_adaptor<
bip::allocator<T, SMgr>
>;
template <typename T> using Vec = boost::container::vector<T, Alloc<T> >;
using String = bip::basic_string<char, std::char_traits<char>, Alloc<char> >;
struct MyStruct {
String data;
using allocator_type = Alloc<char>;
#if 1 // one approach
MyStruct(std::allocator_arg_t, allocator_type, MyStruct const& rhs) : data(rhs.data) {}
template <
typename I,
typename A = Alloc<char>,
typename = std::enable_if_t<not std::is_same_v<MyStruct, I>, void> >
MyStruct(std::allocator_arg_t, A alloc, I&& init)
: data(std::forward<I>(init), alloc.get_segment_manager())
{ }
#else // the simpler(?) approach
MyStruct(MyStruct const& rhs, allocator_type = {}) : data(rhs.data) {}
template <typename I, typename = std::enable_if_t<not std::is_same_v<MyStruct, I>, void> >
MyStruct(I&& init, allocator_type a)
: data(std::forward<I>(init), a)
{ }
#endif
};
using Database = Vec<MyStruct>;
}
namespace std {
// this appears optional for the current use case
template <typename T> struct uses_allocator<Shared::MyStruct, T> : std::true_type {};
}
int main() {
Shared::Segment mf(bip::open_or_create, "test.bin", 10<<20);
auto& db = *mf.find_or_construct<Shared::Database>("db")(mf.get_segment_manager());
db.emplace_back("one");
db.emplace_back("two");
db.emplace_back("three");
std::cout << "db has " << db.size() << " elements:";
for (auto& el : db) {
std::cout << " " << el.data;
}
std::cout << std::endl;
}
Invoking it three times:
db has 3 elements: one two three
db has 6 elements: one two three one two three
db has 9 elements: one two three one two three one two three
Update: More Complicated
In response to the comments, let's make it more complicated in two ways:
- The struct constructor will take various arguments initializing various members, some of which will use an allocator.
- We want to store it in a Map, and some of the use-patterns involving map are pesky with scoped allocator support (emplacement,
map[k]=v
update-assignment with default-construction requirements)
std::initalizer_list<>
will not be deduced in generic forwarding wrappers :(
Defining the struct:
struct MyPodStruct {
using allocator_type = ScopedAlloc<char>;
int a = 0; // simplify default constructor using NSMI
int b = 0;
Vec<uint8_t> data;
explicit MyPodStruct(allocator_type alloc) : data(alloc) {}
//MyPodStruct(MyPodStruct const&) = default;
//MyPodStruct(MyPodStruct&&) = default;
//MyPodStruct& operator=(MyPodStruct const&) = default;
//MyPodStruct& operator=(MyPodStruct&&) = default;
MyPodStruct(std::allocator_arg_t, allocator_type, MyPodStruct&& rhs) : MyPodStruct(std::move(rhs)) {}
MyPodStruct(std::allocator_arg_t, allocator_type, MyPodStruct const& rhs) : MyPodStruct(rhs) {}
template <typename I, typename A = Alloc<char>>
MyPodStruct(std::allocator_arg_t, A alloc, int a, int b, I&& init)
: MyPodStruct(a, b, Vec<uint8_t>(std::forward<I>(init), alloc)) { }
private:
explicit MyPodStruct(int a, int b, Vec<uint8_t> data) : a(a), b(b), data(std::move(data)) {}
};
It addresses "default construction" (under uses-allocator regime), and the various constructors that take multiple arguments. Not that SFINAE is no longer required to disambiguate the uses-allocator copy-constructor, because the number of arguments differs.
Now, using it is more involved than above. Specifically, since there are multiple constructor arguments to be forwarded, we need another bit of "construction protocol": std::piece_wise_construct_t
.
The inline comments talk about QoL/QoI concerns and pitfalls:
int main() {
using Shared::MyPodStruct;
Shared::Segment mf(bip::open_or_create, "test.bin", 10<<10); // smaller for Coliru
auto mgr = mf.get_segment_manager();
auto& db = *mf.find_or_construct<Shared::Database>("complex")(mgr);
// Issues with brace-enclosed initializer list
using Bytes = std::initializer_list<uint8_t>;
// More magic: piecewise construction protocol :)
static constexpr std::piecewise_construct_t pw{};
using std::forward_as_tuple;
db.emplace(pw, forward_as_tuple("one"), forward_as_tuple(1,2, Bytes {1,2}));
db.emplace(pw, forward_as_tuple("two"), forward_as_tuple(2,3, Bytes {4}));
db.emplace(pw, forward_as_tuple("three"), forward_as_tuple(3,4, Bytes {5,8}));
std::cout << "\n=== Before updates\n" << db << std::endl;
// Clumsy:
db[Shared::String("one", mgr)] = MyPodStruct{std::allocator_arg, mgr, 1,20, Bytes {7,8,9}};
// As efficient or better, and less clumsy:
auto insert_or_update = [&db](auto&& key, auto&&... initializers) -> MyPodStruct& {
// Be careful not to move twice: https://en.cppreference.com/w/cpp/container/map/emplace
// > The element may be constructed even if there already is an element
// > with the key in the container, in which case the newly constructed
// > element will be destroyed immediately.
if (auto insertion = db.emplace(pw, forward_as_tuple(key), std::tie(initializers...)); insertion.second) {
return insertion.first->second;
} else {
return insertion.first->second = MyPodStruct(
std::allocator_arg,
db.get_allocator(),
std::forward<decltype(initializers)>(initializers)...); // forwarding ok here
}
};
insert_or_update("two", 2,30, Bytes{});
insert_or_update("nine", 9,100, Bytes{5,6});
// partial updates:
db.at(Shared::String("nine", mgr)).data.push_back(42);
// For more efficient key lookups in the case of unlikely insertion, use
// heterogeneous comparer, see https://stackoverflow.com/a/27330042/85371
std::cout << "\n=== After updates\n" << db << std::endl;
}
Which prints Live On Coliru
=== Before updates
db has 3 elements: {one: 1,2, [1,2,]} {three: 3,4, [5,8,]} {two: 2,3, [4,]}
=== After updates
db has 4 elements: {nine: 9,100, [5,6,42,]} {one: 1,20, [7,8,9,]} {three: 3,4, [5,8,]} {two: 2,30, []}
Full Listing
For conservation: Live On Coliru
#include <boost/interprocess/containers/map.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <boost/interprocess/containers/vector.hpp>
#include <boost/interprocess/managed_mapped_file.hpp>
#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/container/scoped_allocator.hpp>
#include <iostream>
namespace bip = boost::interprocess;
namespace Shared {
using Segment = bip::managed_mapped_file;
using SMgr = Segment::segment_manager;
template <typename T> using Alloc = bip::allocator<T, SMgr>;
template <typename T> using ScopedAlloc = boost::container::scoped_allocator_adaptor<Alloc<T> >;
using String = bip::basic_string<char, std::char_traits<char>, Alloc<char> >;
using boost::interprocess::map;
template <typename T> using Vec =
boost::container::vector<T, ScopedAlloc<T>>;
template <typename K, typename T> using Map =
map<K, T, std::less<K>, ScopedAlloc<typename map<K, T>::value_type>>;
struct MyPodStruct {
using allocator_type = ScopedAlloc<char>;
int a = 0; // simplify default constructor using NSMI
int b = 0;
Vec<uint8_t> data;
explicit MyPodStruct(allocator_type alloc) : data(alloc) {}
//MyPodStruct(MyPodStruct const&) = default;
//MyPodStruct(MyPodStruct&&) = default;
//MyPodStruct& operator=(MyPodStruct const&) = default;
//MyPodStruct& operator=(MyPodStruct&&) = default;
MyPodStruct(std::allocator_arg_t, allocator_type, MyPodStruct&& rhs) : MyPodStruct(std::move(rhs)) {}
MyPodStruct(std::allocator_arg_t, allocator_type, MyPodStruct const& rhs) : MyPodStruct(rhs) {}
template <typename I, typename A = Alloc<char>>
MyPodStruct(std::allocator_arg_t, A alloc, int a, int b, I&& init)
: MyPodStruct(a, b, Vec<uint8_t>(std::forward<I>(init), alloc)) { }
private:
explicit MyPodStruct(int a, int b, Vec<uint8_t> data) : a(a), b(b), data(std::move(data)) {}
};
using Database = Map<String, MyPodStruct>;
static inline std::ostream& operator<<(std::ostream& os, Database const& db) {
os << "db has " << db.size() << " elements:";
for (auto& [k,v] : db) {
os << " {" << k << ": " << v.a << "," << v.b << ", [";
for (unsigned i : v.data)
os << i << ",";
os << "]}";
}
return os;
}
}
int main() {
using Shared::MyPodStruct;
Shared::Segment mf(bip::open_or_create, "test.bin", 10<<10); // smaller for Coliru
auto mgr = mf.get_segment_manager();
auto& db = *mf.find_or_construct<Shared::Database>("complex")(mgr);
// Issues with brace-enclosed initializer list
using Bytes = std::initializer_list<uint8_t>;
// More magic: piecewise construction protocol :)
static constexpr std::piecewise_construct_t pw{};
using std::forward_as_tuple;
db.emplace(pw, forward_as_tuple("one"), forward_as_tuple(1,2, Bytes {1,2}));
db.emplace(pw, forward_as_tuple("two"), forward_as_tuple(2,3, Bytes {4}));
db.emplace(pw, forward_as_tuple("three"), forward_as_tuple(3,4, Bytes {5,8}));
std::cout << "\n=== Before updates\n" << db << std::endl;
// Clumsy:
db[Shared::String("one", mgr)] = MyPodStruct{std::allocator_arg, mgr, 1,20, Bytes {7,8,9}};
// As efficient or better, and less clumsy:
auto insert_or_update = [&db](auto&& key, auto&&... initializers) -> MyPodStruct& {
// Be careful not to move twice: https://en.cppreference.com/w/cpp/container/map/emplace
// > The element may be constructed even if there already is an element
// > with the key in the container, in which case the newly constructed
// > element will be destroyed immediately.
if (auto insertion = db.emplace(pw, forward_as_tuple(key), std::tie(initializers...)); insertion.second) {
return insertion.first->second;
} else {
return insertion.first->second = MyPodStruct(
std::allocator_arg,
db.get_allocator(),
std::forward<decltype(initializers)>(initializers)...); // forwarding ok here
}
};
insert_or_update("two", 2,30, Bytes{});
insert_or_update("nine", 9,100, Bytes{5,6});
// partial updates:
db.at(Shared::String("nine", mgr)).data.push_back(42);
// For more efficient key lookups in the case of unlikely insertion, use
// heterogeneous comparer, see https://stackoverflow.com/a/27330042/85371
std::cout << "\n=== After updates\n" << db << std::endl;
}