简体   繁体   中英

Deleting polymorphic objects allocated with C++17 pmr memory resource

I would like to build up a tree consisting of polymorphic objects of type Node which are allocated with a custom PMR allocator.

So far, everything functions well, but I cannot figure out how to properly delete polymorphic objects allocated with a non-standard allocator?? I have only come up with a solution to declare a static object holding a reference to a std::pmr::memory_resource .. but that's nasty. Is there any "right" way to delete custom-allocated polymorphic objects?

Here is a self-containing example:

#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <functional>
#include <memory_resource>
        
struct Node {
    // NOTE: this is actually not necessary..
    using allocator_type = std::pmr::polymorphic_allocator<Node>;

    void operator delete(void *ptr, std::size_t sz) noexcept {
        Node::deleter(ptr, sz);
    }
    // don't bother with getters/setters so far..
    std::pmr::string name;

    template <class TNode >
    static TNode *create(std::string_view name, std::pmr::memory_resource *res) {

        std::pmr::polymorphic_allocator<TNode> alloc(res);
        auto ptr = alloc.allocate(1);
        ::new (ptr) TNode(alloc);
        ptr->name = name;
        return ptr;
    }
    virtual ~Node() {
        std::cerr << "Destructing node: " << name << std::endl;
    }

    // NASTY: pointer to memory resource to delete polymorphic objects..
    static std::pmr::memory_resource *s_deleterResource;

protected:
    Node(const allocator_type& alloc) : name(alloc) {}

    static void deleter(void *ptr, std::size_t sz) noexcept {
        if (s_deleterResource != nullptr) {
            std::cerr << "Deleting mem: " << ptr << " of size: " << sz << " using PMR resource\n";
            std::pmr::polymorphic_allocator< char >(s_deleterResource)
                .deallocate((char *)ptr, sz);
        }
        else {
            std::cerr << "Deleting mem: " << ptr << " of size: " << sz << " using default\n";
            ::operator delete(ptr, sz);
        }
    }
};

decltype (Node::s_deleterResource) Node::s_deleterResource = nullptr;

struct CompoundNode : Node {

    friend struct Node;
    using allocator_type = std::pmr::polymorphic_allocator<CompoundNode>;

    void operator delete(void *ptr, std::size_t sz) noexcept {
        Node::deleter(ptr, sz);
    }

    void addChild(Node *child) {
        m_children.push_back(child);
    }

    ~CompoundNode() override {
        for(auto child : m_children) {
            delete child;
        }
    }

protected:
    explicit CompoundNode(const allocator_type& alloc) :
        Node(alloc), m_children(alloc)
    { }

    std::pmr::vector< Node * > m_children;
};

struct LeafNode : Node {

    friend struct Node;
    using allocator_type = std::pmr::polymorphic_allocator<LeafNode>;

    void operator delete(void *ptr, std::size_t sz) noexcept {
        Node::deleter(ptr, sz);
    }
    ~LeafNode() override {
        // NOTE: this is probably won't work since the object m_payload is not yet destroyed
        //allocator_type alloc(m_payload.get_allocator());
        //alloc.deallocate(this, 1);
    }

protected:
    explicit LeafNode(const allocator_type& alloc) :
        Node(alloc), m_payload(77, alloc) { }

    std::pmr::vector< uint8_t > m_payload;
};

// adding verbosity to the existing memory resource
struct VerboseMemResource : public std::pmr::memory_resource {

    VerboseMemResource(const char *name, std::pmr::memory_resource *base)
            : m_name(name), m_resource(base) {  }

private:

    void *do_allocate(std::size_t bytes, std::size_t alignment) override {

        auto ptr = m_resource->allocate(bytes, alignment);
        std::cerr << "Allocated: " << bytes << " bytes with '" << m_name << "': " << ptr << std::endl;
        return ptr;
    }

    void do_deallocate(void *ptr, std::size_t bytes, std::size_t alignment) override {

        std::cerr << "Deallocating " << bytes << " bytes with '" << m_name << "': " << ptr << std::endl;
        return m_resource->deallocate(ptr, bytes, alignment);
    }

    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        return m_resource->is_equal(other);
    }

    std::string m_name;
    std::pmr::memory_resource *m_resource;
};

int main() try
{
    std::array< uint8_t, 1000 > buf;
    std::pmr::monotonic_buffer_resource bufferRes(buf.data(), buf.size(),
                                       std::pmr::null_memory_resource());
    VerboseMemResource res("buffered resource", &bufferRes);

    auto root = Node::create<CompoundNode>("root", &res);
    root->addChild(Node::create<LeafNode>("child1", &res));
    root->addChild(Node::create<LeafNode>("child2", &res));

    // set the pointer to our memory resource for deletion:
    Node::s_deleterResource = &res;
    std::cerr << "Beginning tree deletetion..\n";
    delete root;
    Node::s_deleterResource = nullptr;
    return 0;
}
catch(std::exception& ex) {
    std::cerr << "Exception: " << ex.what() << std::endl;
    return 1;
}

And here is the output:

Allocated: 80 bytes with 'buffered resource': 0000006D21FDF740
Allocated: 80 bytes with 'buffered resource': 0000006D21FDF790
Allocated: 77 bytes with 'buffered resource': 0000006D21FDF7E0
Allocated: 8 bytes with 'buffered resource': 0000006D21FDF830
Allocated: 80 bytes with 'buffered resource': 0000006D21FDF838
Allocated: 77 bytes with 'buffered resource': 0000006D21FDF888
Allocated: 16 bytes with 'buffered resource': 0000006D21FDF8D8
Deallocating 8 bytes with 'buffered resource': 0000006D21FDF830
Beginning tree deletetion..
Deallocating 77 bytes with 'buffered resource': 0000006D21FDF7E0
Destructing node: child1
Deleting mem: 0000006D21FDF790 of size: 80 using PMR resource
Deallocating 80 bytes with 'buffered resource': 0000006D21FDF790
Deallocating 77 bytes with 'buffered resource': 0000006D21FDF888
Destructing node: child2
Deleting mem: 0000006D21FDF838 of size: 80 using PMR resource
Deallocating 80 bytes with 'buffered resource': 0000006D21FDF838
Deallocating 16 bytes with 'buffered resource': 0000006D21FDF8D8
Destructing node: root
Deleting mem: 0000006D21FDF740 of size: 80 using PMR resource
Deallocating 80 bytes with 'buffered resource': 0000006D21FDF740

The problem

Prior to C++20, there was no way to invoke a deallocation function ( operator delete ) that didn't call your class' destructor first, making it impossible for you to clean up extra explicitly allocated resources owned by your class (without hacky code like your static pointer)

The solution

If you have access to C++20, then I encourage you to use destroying delete which was created to solve problems like this.

  • Your class can hold onto an instance of std::pmr::memory_resource* (injected through the constructor)
  • Change your operator delete into eg, void operator delete(Node *ptr, std::destroying_delete_t) noexcept
    • destroying_delete is a tag that, when you use it, indicates that you will take responsibility for invoking the appropriate destructor.
  • Derived classes should also implement a similar deleter.

Without making too many changes to your code, we can do the following in Node :

struct Node {
    // NOTE: this is actually not necessary..
    using allocator_type = std::pmr::polymorphic_allocator<Node>;

    void operator delete(Node *ptr, std::destroying_delete_t) noexcept {
        deleter(ptr);
    }
    // don't bother with getters/setters so far..
    std::pmr::string name;

    template <class TNode >
    static TNode *create(std::string_view name, std::pmr::memory_resource *res) {

        std::pmr::polymorphic_allocator<TNode> alloc(res);
        auto ptr = alloc.allocate(1);
        ::new (ptr) TNode(alloc, res);
        ptr->name = name;
        return ptr;
    }
    virtual ~Node() {
        std::cerr << "Destructing node: " << name << std::endl;
    }

protected:
    Node(const allocator_type& alloc, std::pmr::memory_resource *res)
     : name(alloc), s_deleterResource(res) {}

    std::pmr::memory_resource *s_deleterResource = nullptr;  

    template<class TNode>
     static void deleter(TNode* ptr) noexcept {
        if (ptr->s_deleterResource != nullptr) {
            auto* deleterResource = ptr->s_deleterResource;
            ptr->~TNode();
            std::cerr << "Deleting mem: " << ptr  << " using PMR resource\n";
            std::pmr::polymorphic_allocator< TNode >(deleterResource)
                .deallocate(ptr, 1);
        }
        else {
            std::cerr << "Deleting mem: " << ptr << " using default\n";
            ::delete ptr;
        }
    }
};

And then in eg, LeafNode you can write this:

struct LeafNode : Node {
    friend struct Node;
    using allocator_type = std::pmr::polymorphic_allocator<LeafNode>;

    void operator delete(LeafNode *ptr, std::destroying_delete_t) noexcept {
        deleter(ptr);
    }

protected:
    explicit LeafNode(const allocator_type& alloc, std::pmr::memory_resource *res) :
        Node(alloc, res), m_payload(77, alloc) { }

    std::pmr::vector< uint8_t > m_payload;
};

Live Demo

Allocated: 88 bytes with 'buffered resource': 0x7ffebb5906d0
Allocated: 88 bytes with 'buffered resource': 0x7ffebb590728
Allocated: 77 bytes with 'buffered resource': 0x7ffebb590780
Allocated: 8 bytes with 'buffered resource': 0x7ffebb5907d0
Allocated: 88 bytes with 'buffered resource': 0x7ffebb5907d8
Allocated: 77 bytes with 'buffered resource': 0x7ffebb590830
Allocated: 16 bytes with 'buffered resource': 0x7ffebb590880
Deallocating 8 bytes with 'buffered resource': 0x7ffebb5907d0
Beginning tree deletetion..
Deallocating 77 bytes with 'buffered resource': 0x7ffebb590780
Destructing node: child1
Deleting mem: 0x7ffebb590728 using PMR resource
Deallocating 88 bytes with 'buffered resource': 0x7ffebb590728
Deallocating 77 bytes with 'buffered resource': 0x7ffebb590830
Destructing node: child2
Deleting mem: 0x7ffebb5907d8 using PMR resource
Deallocating 88 bytes with 'buffered resource': 0x7ffebb5907d8
Deallocating 16 bytes with 'buffered resource': 0x7ffebb590880
Destructing node: root
Deleting mem: 0x7ffebb5906d0 using PMR resource
Deallocating 88 bytes with 'buffered resource': 0x7ffebb5906d0

(Notice that the class is a little bit bigger because it holds onto a pointer instead of that pointer being static )

Actually, I found the way to do the same what AndyG did but in C++17 (without using nasty static member Node::s_deleterResource and without the missing std::destroying_delete_t ).

The idea is to introduce a pure virtual function size() which returns a size of *this for each subclass. Then, call the child's destructor explicitly and deallocate the required number of bytes. Below is the modified demo program. There, I introduced a top-level RootNode which is the only one node type which can be created on the stack: all other nodes are created using a given allocator as before:

#include <iostream>
#include <string>
#include <vector>
#include <array>
#include <functional>
#include <memory_resource>

struct CompoundNode;

struct Node {

    friend struct CompoundNode;
    // NOTE: this is actually not necessary..
    using allocator_type = std::pmr::polymorphic_allocator<Node>;

    // don't bother with getters/setters so far..
    std::pmr::string name;

    virtual ~Node() {
        std::cerr << "Destructing node: " << name << std::endl;
    }
    // function returning size of this object
    virtual size_t size() const noexcept = 0;

protected:
    Node(const allocator_type& alloc) : name(alloc) {}
};

struct CompoundNode : Node {

    using allocator_type = std::pmr::polymorphic_allocator<CompoundNode>;

    size_t size() const noexcept override {
        return sizeof(CompoundNode);
    }

    template <class TNode >
    TNode *addChild(std::string_view name) {

        std::pmr::polymorphic_allocator<TNode> alloc(m_children.get_allocator());
        auto ptr = alloc.allocate(1);
        ::new (ptr) TNode(alloc);
        ptr->name = name;
        m_children.push_back(ptr);
        return ptr;
    }

    ~CompoundNode() override {

        std::cerr << "Destructing CompoundNode..\n";
        std::pmr::polymorphic_allocator<char> alloc(m_children.get_allocator());

        for(auto child : m_children)
        {
            std::cerr << "Destructing child: " << child->name << '\n';
            auto sz = child->size();             // get our size
            child->~Node();                      // call virtual destructor
            alloc.deallocate((char *)child, sz); // free memory
        }
    }

protected:
    explicit CompoundNode(const allocator_type& alloc) :
        Node(alloc), m_children(alloc)
    { }

    std::pmr::vector< Node * > m_children;
};

struct LeafNode : Node {
    friend struct CompoundNode;

    using allocator_type = std::pmr::polymorphic_allocator<LeafNode>;

    size_t size() const noexcept override {
        return sizeof(LeafNode);
    }

    ~LeafNode() override = default;

protected:
    explicit LeafNode(const allocator_type& alloc) :
        Node(alloc), m_payload(77, alloc) { }

    std::pmr::vector< uint8_t > m_payload;
};

struct RootNode : CompoundNode
{
    //! NOTE: root node does not use a polymorphic allocator itself:
    //! it only provides it for derived classes!
    explicit RootNode(std::pmr::memory_resource *mem) noexcept :
            CompoundNode(mem) {
        name = "root";
    }

    ~RootNode() override = default;
};


// adding verbosity to the existing memory resource
struct VerboseMemResource : public std::pmr::memory_resource {

    VerboseMemResource(const char *name, std::pmr::memory_resource *base)
            : m_name(name), m_resource(base) {  }

private:

    void *do_allocate(std::size_t bytes, std::size_t alignment) override {

        auto ptr = m_resource->allocate(bytes, alignment);
        std::cerr << "Allocated: " << bytes << " bytes with '" << m_name << "': " << ptr << std::endl;
        return ptr;
    }

    void do_deallocate(void *ptr, std::size_t bytes, std::size_t alignment) override {

        std::cerr << "Deallocating " << bytes << " bytes with '" << m_name << "': " << ptr << std::endl;
        return m_resource->deallocate(ptr, bytes, alignment);
    }

    bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override {
        return m_resource->is_equal(other);
    }

    std::string m_name;
    std::pmr::memory_resource *m_resource;
};

int main() try
{
    std::array< uint8_t, 1000 > buf;
    std::pmr::monotonic_buffer_resource bufferRes(buf.data(), buf.size(),
                                       std::pmr::null_memory_resource());
    VerboseMemResource res("buffered resource", &bufferRes);

    {
        RootNode root(&res);
        auto comp = root.addChild<CompoundNode>("compound");
        comp->addChild<LeafNode>("child1");
        comp->addChild<LeafNode>("child2");
    }

    return 0;
}
catch(std::exception& ex) {
    std::cerr << "Exception: " << ex.what() << std::endl;
    return 1;
}

And the resulting output:

Allocated: 80 bytes with 'buffered resource': 000000874DAFF900
Allocated: 8 bytes with 'buffered resource': 000000874DAFF950
Allocated: 80 bytes with 'buffered resource': 000000874DAFF958
Allocated: 77 bytes with 'buffered resource': 000000874DAFF9A8
Allocated: 8 bytes with 'buffered resource': 000000874DAFF9F8
Allocated: 80 bytes with 'buffered resource': 000000874DAFFA00
Allocated: 77 bytes with 'buffered resource': 000000874DAFFA50
Allocated: 16 bytes with 'buffered resource': 000000874DAFFAA0
Deallocating 8 bytes with 'buffered resource': 000000874DAFF9F8
Destructing CompoundNode..
Destructing child: compound
Destructing CompoundNode..
Destructing child: child1
Deallocating 77 bytes with 'buffered resource': 000000874DAFF9A8
Destructing node: child1
Deallocating 80 bytes with 'buffered resource': 000000874DAFF958
Destructing child: child2
Deallocating 77 bytes with 'buffered resource': 000000874DAFFA50
Destructing node: child2
Deallocating 80 bytes with 'buffered resource': 000000874DAFFA00
Deallocating 16 bytes with 'buffered resource': 000000874DAFFAA0
Destructing node: compound
Deallocating 80 bytes with 'buffered resource': 000000874DAFF900
Deallocating 8 bytes with 'buffered resource': 000000874DAFF950
Destructing node: root

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM