Search code examples
c++memory-managementpolymorphismc++17allocator

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

Solution

  • 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 e.g., 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 e.g., 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)