I'm not exactly sure if this is technically an "arena allocator" but it serves a similar purpose: providing a fast way to allocate a lot of objects that can all be freed at once.
#include <type_traits>
#include <utility>
#include <new>
template <std::size_t BlockSizeIn = 1024>
struct BlockProviderNewDelete
{
static constexpr std::size_t BlockSize = BlockSizeIn;
static void* allocateBlock()
{
return ::operator new(BlockSize);
}
static void freeBlock(void *block) noexcept
{
::operator delete(block);
}
};
template <class BlockProviderIn>
class ArenaAllocator
{
public:
using BlockProvider = BlockProviderIn;
private:
struct AllocBlock
{
AllocBlock *next;
char mem[BlockProvider::BlockSize - sizeof(AllocBlock*)];
};
AllocBlock *base;
AllocBlock *headBlock;
std::size_t headIndex;
void appendBlock()
{
AllocBlock *newBlock = static_cast<AllocBlock*>(BlockProvider::allocateBlock());
newBlock->next = nullptr;
headBlock->next = newBlock;
headBlock = newBlock;
headIndex = 0;
}
void nextBlock()
{
if(headBlock->next)
{
headBlock = headBlock->next;
headIndex = 0;
}
else
appendBlock();
}
public:
static constexpr std::size_t BlockSize = BlockProvider::BlockSize;
static constexpr std::size_t MaxAllocationSize = sizeof(AllocBlock::mem);
ArenaAllocator()
{
base = static_cast<AllocBlock*>(BlockProvider::allocateBlock());
base->next = nullptr;
headBlock = base;
headIndex = 0;
}
~ArenaAllocator()
{
AllocBlock *cur = base, *next;
while(cur != nullptr)
{
next = cur->next;
BlockProvider::freeBlock(cur);
cur = next;
}
}
void reset() noexcept
{
headBlock = base;
headIndex = 0;
}
void* allocate(std::size_t s)
{
if(s == 0) s = 1;
if(s > MaxAllocationSize)
return nullptr;
else if(s > MaxAllocationSize - headIndex)
nextBlock();
void *ptr = &headBlock->mem[headIndex];
headIndex += s;
if(headIndex >= MaxAllocationSize)
nextBlock();
return ptr;
}
template <typename T, typename... Args>
T* construct(Args... args)
{
static_assert(std::is_trivially_destructible<T>::value, "Type must be trivially destructible!");
static_assert(sizeof(T) <= MaxAllocationSize, "Type must not be larger than max allocation size!");
T *result = static_cast<T*>(allocate(sizeof(T)));
new (result) T(std::forward<Args>(args)...);
return result;
}
};
template <std::size_t BlockSize>
using ArenaAllocatorDefault = ArenaAllocator<BlockProviderNewDelete<BlockSize>>;
A caveat to this is that types allocated with the arena allocator must be trivially destructible (i.e. their memory must be reusable without destroying them first).
I'm already aware of one major issue: it doesn't consider alignment. However that's a fairly easy fix, so I'd like that to be ignored in the review. I'm mostly wondering whether this invokes any undefined behavior (other than due to ignoring memory alignment).
Here's an example of usage (this was to test if it leaked any blocks of memory or segfaulted. It didn't.):
// 2 MiB block size
ArenaAllocatorDefault<2097152> test;
for(size_t i = 0; i < 100000; ++i)
{
for(size_t j = 0; j < 1000000; ++j)
{
test.construct<char[128]>();
}
test.reset();
}
Obviously you'd want to actually use the pointer returned by the construct()
method for usual purposes but this was just for testing.