【问题标题】:Using boost's scoped_allocator_adaptor for shared memory container将 boost 的 scoped_allocator_adaptor 用于共享内存容器
【发布时间】:2019-12-17 11:50:38
【问题描述】:

我正在编写一个 C++17 应用程序,我需要管理一个 STL 或 boost::collections 共享内存中的等效数据结构。

我不确定最简单的语法(避免传递分配器 地方)来创建和更新共享数据结构。

我已经搜索了一段时间,但不是一个琐碎的 String->String map,专注于自定义数据结构或 POD 结构的示例很难出现 经过。 (我怀疑与 POD 结构相关的分配器将是公平的 容易,因为这些可以从连续的内存中分配,因此可以使用 简单的字符分配器 - 相当于下面的Shared::Alloc<char>)。

据我了解,管理数据结构集合的关键是 共享内存以正确选择 stateful allocators 为中心 以及让该分配器与其嵌套的子级共享的能力。

例如,假设我有一个map<Shared::String, vector<Shared::String>> 在共享内存中,scoped_allocator_adaptor 的魔力会起作用。

除了上面map<SHMString, vector<String>> 的简单示例之外,我还会 真的很想管理一个map<SHMString, vector<UserStruct>> 哪里UserStruct 可以 可以是 POD 结构,也可以是包含 StringList 字符串的结构。

我从以下作为另一个答案的有用起点开始 我在 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;
}

看起来Shared:Scoped 分配器适配器是传播 分配器从顶级容器到它的子容器。我不确定这是不是 应用于增强容器与标准容器时有所不同。

一个例子,并解释了如何以一种方式构造这些对象 将允许我将 scoped_allocator_adaptor 传播到我的 POD 或自定义 struct 是我正在寻找的。​​p>

【问题讨论】:

    标签: c++ boost shared-memory boost-interprocess


    【解决方案1】:

    为星星射击,我们是不是 :) 无痛分配器传播是圣杯。

    看起来 Shared:Scoped 分配器适配器是将分配器从顶级容器传播到其子容器的关键。

    确实

    我不确定这在应用于增强容器与标准容器时是否不同。

    据我了解,现代 C++ 标准库应该支持相同的功能,但在实践中,我的经验表明它经常与 Boost Container 容器一起使用。 (YMMV 和标准库实现可能/将会赶上)

    做什么

    我想你会想了解uses_allocator 协议:https://en.cppreference.com/w/cpp/memory/uses_allocator

    我想这确实回答了你所有的问题。如果可以的话,我会尝试提供一个快速示例。

    演示

    到目前为止,我已经采用了以下两种方法:

    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)
        { }
    };
    

    这允许:

    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");
    

    稍微复杂/通用 (?) 的方法也适用:

        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())
        { }
    

    似乎对于当前用例,内部 typedef allocator_type 足以表明MyStruct 支持分配器构造,从而使uses_allocator&lt;MyStruct, ...&gt; 的特化变得多余。

    完整列表

    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;
    }
    

    调用它三次:

    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
    

    更新:更复杂

    针对cmets,让我们从两个方面让它变得更复杂:

    • struct 构造函数将采用各种参数来初始化各种成员,其中一些使用分配器。
    • 我们想将它存储在 Map 中,而涉及 map 的一些使用模式是讨厌,具有范围分配器支持(放置,map[k]=v update-assignment 具有默认构造要求)
    • std::initalizer_list&lt;&gt; 不会在通用转发包装器中推断出来:(

    定义结构:

    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)) {}
    };    
    

    它解决了“默认构造”(在使用分配器机制下)以及采用多个参数的各种构造函数。并不是说不再需要 SFINAE 来消除 uses-allocator 复制构造函数的歧义,因为参数的数量不同。

    现在,使用它比上面更复杂。具体来说,由于要转发多个构造函数参数,我们需要另外一点“构造协议”:std::piece_wise_construct_t

    在线 cmets 谈论 QoL/QoI 问题和陷阱:

    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;
    }
    

    哪个打印 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, []}
    

    完整列表

    保护: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;
    }
    

    【讨论】:

    • 是的,一个例子会很棒,谢谢。像我这样的非元程序员很难理解 uses_allocator 协议。到目前为止,您的示例似乎是唯一好的示例,您可能认出了 Shared 命名空间 :)。
    • 我确实认出了它,确实;让我感到惊讶的是,有这么多出色的程序员构建了具有这些令人敬畏的功能的库,但似乎没有人知道/不愿意展示如何使用它们。
    • 我认为这解决了问题(具有讽刺意味的是,它不再适合这个问题,因为结构不使用任何分配器:)):Live On Coliru。注意about efficiency 的评论,我喜欢你在 cmets 中所做的观察。它们对于理解这一点非常有帮助(是的,例如,使用分配器构造需要安置!)。请注意,像m[k] = v; 这样的一些流行的地图访问模式不再那么适合了。请参阅代码建议。
    • 用更复杂的用例扩展了答案。 Demo On Coliru
    • @johnco3 干杯。你学到了很多东西,而且我知道我花了几年的时间来理解这个“适用性”级别的东西。也就是说,在这一点上,我认为生成的代码非常做作,编写一组执行必要舞蹈的访问器函数可能会变得更明智,也许没有分段和/或使用分配器构造的魔力。很高兴知道它们是如何工作的,但其中涉及相当大的复杂性,并且在维护方面总是需要权衡取舍。祝你好运!
    猜你喜欢
    • 2014-11-30
    • 1970-01-01
    • 2015-04-20
    • 2023-03-11
    • 1970-01-01
    • 2019-02-24
    • 1970-01-01
    • 2012-09-18
    • 1970-01-01
    相关资源
    最近更新 更多