Reputation: 2652
I am writing a C++17 application and I need to manage an STL or boost::collections equivalent data structure in shared memory.
I'm not sure of the simplest syntax (which avoids passing allocators all over the place) to create and update the shared data structure.
I have been searching for some time, but other than a trivial String->String
map, examples focused on custom data structures or POD structs are hard to come
by. (I suspect that the allocator associated with POD structs would be fairly
easy, as these can be allocated from contiguous memory, and therefore could use
the simple char allocator - equivalent to Shared::Alloc<char>
below).
From what I understand, the key to managing collections of data structures in shared memory centers around the correct choice of a stateful allocators and the ability to have that allocator shared with its nested children.
So for example, let's say I have a map<Shared::String, vector<Shared::String>>
in shared memory, somehow the magic of the scoped_allocator_adaptor
would work.
Beyond the simple example of a map<SHMString, vector<String>>
above, I would
really like to manage a map<SHMString, vector<UserStruct>>
where UserStruct
can
be either a POD struct or a struct containing a String
or List
of strings.
I started out with the following as a useful starting point from another answer I found in SO:
namespace bip = boost::interprocess;
namespace Shared {
using Segment = bip::managed_shared_memory;
template <typename T>
using Alloc = bip::allocator<T, Segment::segment_manager>;
using Scoped = boost::container::scoped_allocator_adaptor<Alloc<char>>;
using String = boost::container::basic_string<char, std::char_traits<char>, Scoped>;
using KeyType = String;
}
It looks like the Shared:Scoped
allocator adapter is key to propagating the
allocator from a top level container to its children. I'm not sure if this is
different when applied to the boost containers vs the standard containers.
An example, and explanation on how to construct these objects in a way that
would allow me to propagate the scoped_allocator_adaptor
to my POD or custom
struct is what I am looking for.
Upvotes: 2
Views: 952
Reputation: 393694
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)
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.
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 thatMyStruct
supports allocator-construction, making the specialization ofuses_allocator<MyStruct, ...>
redundant.
#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
In response to the comments, let's make it more complicated in two ways:
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, []}
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;
}
Upvotes: 4