1

Here is my scenario:

There is a set of Instance objects, the number of the objects being determined at run time.

For each of those instances, a custom heap is created. (Yes, I refer to ::HeapCreate() et. all win32 functions).

struct Instance
{
   HANDLE heap;
};

Now, there is some Instance specific State object which has access (a pointer) to the Instance. Its a 1:1 relationship. 1 State per Instance.

struct State
{
    Instance * inst;

};

In that State, I want to use STL containers like std::vector and alike, but I want those to use an allocator, which uses the heap created by the instance.

Reading up on the history of std::allocator from the times of C++98 up to C++17 and newer, some old requirements have been dropped, but my take away is, that it is not possible to write such an allocator for my above shown use case. Given that the Allocator would be of the same type each time, but stateful (as in instances using different heap HANDLEs).

So here my question(s):

  • Can I/ should I even try to use the STL allocator stuff for this use case or better just roll my select few own container classes instead?
  • I stumbled across that std::pmr C++17 namespace and wonder if there is stuff in there I am not yet acquainted with, which would help me with my use case.

Your answers will be very valuable if you show the "idiomatic" way how to solve this problem in modern (c++17 and newer) C++.

Here, my incomplete and current "work in progress". I will fill in more over time, based on answers and comments and my own progress.

#include <Windows.h>

struct custom_heap_allocator
{
    HANDLE heap;

    // No default constructor
    custom_heap_allocator() = delete;

    // an instance is tied to a heap handle.
    explicit custom_heap_allocator(HANDLE h)
        : heap{ h }
    {

    }

    // can I get away with this....

    // copy constructor for same type.
    custom_heap_allocator(const custom_heap_allocator& other)
        : heap{ other.heap }
    {

    }

    //... or do I need something like this? Or is it somehow covered by rebind?
    //template<class U>
    //custom_heap_allocator(const custom_heap_allocator<U>& other)
    //  : heap{ other.heap }
    //{

    //}

    template<class U>
    struct rebind {
        typedef custom_heap_allocator other;
    };
};

template <class T, class U>
constexpr bool operator== (const custom_heap_allocator& a1, const custom_heap_allocator& a2) noexcept
{
    return a1.heap == a2.heap;
}

template <class T, class U>
constexpr bool operator!= (const custom_heap_allocator& a1, const custom_heap_allocator& a2) noexcept
{
    return a1.heap != a2.heap;
}

Note: shutting down an Instance will simply destroy the heap (no need to call destructors for stuff still on the heap, as those are just simple data and not system resources etc. And all dynamic stuff they hold are also from the same heap instance).

BitTickler
  • 10,905
  • 5
  • 32
  • 53
  • Can you be a little more specific about why you think allocators would *not* work as you have described? It looks like all containers support stateful allocators since C++11. For example, you can pass an instance of your chosen allocator type to the constructor of std::vector. The vector will store a copy of your allocator to use for any allocations. – Anton Dec 24 '19 at 05:27
  • There is still the requirement holding, I think, that ``operator==()`` of same typed allocators has to return true, right? And all examples in blogs etc. I found use static member variables for their state. – BitTickler Dec 24 '19 at 05:28
  • @Anton Also, rebind deals in types, not in instances of allocators, I think. – BitTickler Dec 24 '19 at 05:34
  • The requirement is that all allocators created by copying are equivalent, and that allocators can deallocate any memory allocated by another equivalent allocator. I don't think it implies that *every* instance of an allocator must be equivalent. See this question: https://stackoverflow.com/q/24278803/41704 – Anton Dec 24 '19 at 05:39
  • "rebind" is a mechanism for containers to obtain a "related" allocator for other types. If your allocator is not type-specific (that is, it allocates all types in the same way / from the same heap) then rebind should result in the same type, which can be copy-constructed from the original allocator to make it equivalent. – Anton Dec 24 '19 at 05:50
  • To rephrase your doubts in a concise manner: stateful allocators are not really stateful. Do you think this has any basis in reality? – n. m. could be an AI Dec 24 '19 at 06:16
  • @n.'pronouns'm. In my reality (and we are touching deeply philosophical questions about objective reality here), my allocator does not even have to be templated. I read somewhere they need not be but signatures of member functions like ``allocate()`` hint otherwise. What I try to do is "have an allocator instance (defined by the value of its heap member variable), containers use for all kinds of types". This simple concept seems to collide with STL library designer realities on many levels... – BitTickler Dec 24 '19 at 06:38
  • The word "stateful" means "has a state". If you think all allocators, regardless of the state, are equivalent, then what does having a state ever mean? – n. m. could be an AI Dec 24 '19 at 06:43
  • @n.'pronouns'm. You probably mean "all allocators of same type" But you could also mean "all allocators of the type "family" ``my_allocator``" or you could indeed mean all allocators. – BitTickler Dec 24 '19 at 06:46
  • Yes of the same type of course. – n. m. could be an AI Dec 24 '19 at 10:22

1 Answers1

1

Here is a more complete version of the allocator from your (edited) question.

Some basic unit tests using std::vector and std::list at cpp.sh/4viaj

template<typename T>
struct custom_heap_allocator
{
    using value_type = T;
    using pointer = T*;
    using const_pointer = const T*;
    using reference = T&;
    using const_reference = const T&;

    // No default constructor
    custom_heap_allocator() = delete;

    // An instance is tied to a heap handle.
    explicit custom_heap_allocator(HANDLE h)
        : heap{ h }
    {
    }

    // Instances are copy-constructable and copy-assignable.
    custom_heap_allocator(const custom_heap_allocator&) = default;
    custom_heap_allocator& operator=(const custom_heap_allocator&) = default;

    // All related allocators share the same heap, regardless of type.
    template<class U>
    custom_heap_allocator(const custom_heap_allocator<U>& other)
      : heap{ other.heap }
    {
    }

    // Allocate and deallocate space for objects using the heap.
    T* allocate(size_t n) {
        return static_cast<T*>(HeapAlloc(heap, 0, sizeof(T) * n)); 
    }
    void deallocate(T* ptr, size_t n) {
        HeapFree(heap, 0, ptr);
    }

    // Construct and destroy objects in previously allocated space.
    // This *should* be optional and provided by std::allocator_traits,
    // but it looks like some std containers don't use the traits.
    template< class U, class... Args >
    void construct( U* p, Args&&... args ) {
        ::new((void *)p) U(std::forward<Args>(args)...);
    }
    template< class U >
    void destroy( U* p ) {
        p->~U();
    }

    // Template for related allocators of different types.
    template<class U>
    struct rebind {
        typedef custom_heap_allocator<U> other;
    };

private:
    // Heap used for all allocations/deallocations.
    HANDLE heap;

    // Allow all related types to access our private heap.
    template<typename> friend struct custom_heap_allocator;
};
BitTickler
  • 10,905
  • 5
  • 32
  • 53
Anton
  • 3,170
  • 20
  • 20
  • In last line, you need to write ``friend struct``... instead of ``friend class`` else it rains a hailstorm of warnings. Did not run it yet...but wrote a test case. – BitTickler Dec 24 '19 at 07:52
  • Still warns a lot. For the full story, see: https://gist.github.com/ruffianeo/bd814e407fbb1404ccdf944ab26d6fa5 In first version I had an exception due to a missing scope. Deleting heap before destructors not a good idea... – BitTickler Dec 24 '19 at 08:25
  • 1
    both ``allocate()`` and ``deallocate()`` need a ``size_t n`` instead of ``int n``. Then the warnings go away. (Compiling 64 bit and windows size_t is 64 bit on that platform). – BitTickler Dec 24 '19 at 08:30