The C++ allocator system is a woeful mess. In the nurtl repository I have a new system
for allocators. It is intended for general and real time use.
The base abstraction is trivial, there are two methods:
virtual void *allocate(size_t)=0;
virtual void deallocate(void *, size_t)=0;
To constructy an object call
void *operator new(size_t amt, allocator_t& a) { return a.allocate (amt); }
Now things get a bit messy. To delete an object there are two functions:
template<class T>
void delete_concrete_object (T *object, allocator_t *a) {
object->~T(); a->deallocate(object, sizeof(T));
};
template<class T>
void delete_csp_polymorphic_object (T *object, allocator_t *a) {
size_t amt = object->size();
object->~T();
a->deallocate(object, amt);
};
The first function uses the type of the pointer to calculate the
object size for you. The second function requires the object
have a method size() that returns the size of the complete type.
Note because this is a moronic C++ template system, it suffices
that the base class define
virtual size_t size()const=0;
that is, there is no need for a universal base. In C++20 there might
be a workaround: C++ is stupid in not providing this method automatically
for all polymorphic objects (i.e. ones with a virtual destructor) since the RTTI
necessarily contains that information (otherwise destructors wouldn’t work).
The onus is on the client to call the right function. Correct deallocation of
non-polymorphic heap allocated derived class types is impossible in general.
Luckily such types are also totally useless unless referenced by their
concrete type (since there is no virtual dispatch).
=======================================================
So what I have shown above is only the FIRST APPROXIMATION.
In the second approximation, I have made allocators reference counted
and provided a dedicated smart pointer type:
struct alloc_ref_t {
….
The new and delete methods above also support this type.
Note, it can have a nullptr.
========================================
But that is ALSO an approximation.
Here is the actual design. I hope you read this far because the stuff above
is boring and pointless. The next bit is novel.
Allocators can have a PARENT which is another allocator. It can be null.
We use, of course, the smart pointer alloc_ref_t. This ensure the parent
allocator exists as long as the child does.
So what’s the point??
There are several applications. The main one is ordinary delegation:
1. A thread safe allocator uses spinlocks around calls to its parent
which is not thread safe.
2. A debugging allocator to any other allocator.
3. A statistics allocator. This is VITAL. It keeps track
of the maximum number of blocks of each size allocated
at any one time but then calls malloc_free or the C++ allocator.
The destructor saves the data to a file. The data is REQUIRED
to help set up the real time allocator which has a fixed number
of pre-allocated blocks of each size and crashes is you ask
for too many (the crash is essential for performance, the allocator
checks nothing).
But there is another type of allocator which is very important:
a SUBALLOCATOR.
A suballocator grabs large blocks from its parent, and suballocates them.
The current nurlt system_allocator suballocates in a radical
way: it calls its allocate method of its parent exactly once during construction
and deallocate on destruction. It then suballocates that block according to
a map of blocks size/count pairs fed to it on constuction.
However this is not the only possible kind of suballocator.
Most importantly, if a sequential (single threaded) computation
needs temporary storage for, say, a list, we can dynamically
construct a non-thread safe (read: blindingly fast) suballocator
parented on our current thread safe allocator. This avoids the locks.
Indeed in some cases an ARENA allocator can be used.
This is a brain dead allocator that simply bumps a pointer to do
an allocation and totally ignores deallocation requests.
return p+=n;
is pretty dang fast! When finished, the destructor returns the arena
to the parent.
=============================================
AND FINALLY. The real crux of the problem!!!!!!!!!!!!
HOW DO WE DELETE AN ALLOCATOR?????
There are two answers.
1. Store the allocator required IN the allocator.
2. Use the parent.
The second option is simpler in principle though less general,
however it may complicate usage.
The problem is that if we use the system allocator, for example,
we have to reserve space for the child allocator AND everything
that child allocator needs, this is THREE kinds of object:
(a) the top level allocator object
(b) infrastructure required to implement the operation
(c) the actual memory required
The stuff in (b) and (c) is handled by the allocator itself
in its constructor, destructor, and operation, as required,
but for (a) someone else has made the allocator object.
But the deletion is typically by suicide or, more or less
equivalently, by a smart pointer. So the alloc_ref_t smart
pointer has to know how to delete an allocaator.
Of course it will call
delete_csp_polymorphic_object(
allocator_to_delete,
allocator_to_delete_with // which is what?
);
The sane answer is
delete_csp_polymorphic_object(a, a->parent)
More precisely if the allocator is not null AND its
parent is not null use the parent, otherwise use
the C++ system allocator (i.e. just call delete).
—
John Skaller
ska...@internode.on.net