Objects like std::vector have to reserve space for their stored objects. By default, they get that memory from the global heap. A special allocator can provide you with a different source of memory.
The allocator is a class that defines two important functions. The first is the allocate member function, which gives back storage for a number of objects. The second is the deallocate member function, which gives back this storage when it is no longer in use.
Here's a minimal allocator that forwards to the global operators.
#include <iostream>
#include <vector>
#include <memory>
template <typename T>
struct SimpleAllocator
{
using value_type = T;
T* allocate(std::size_t n)
{
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t)
{
// Declare delete before code needs to call it
::operator delete(p);
}
};
int main()
{
std::vector<int, SimpleAllocator<int>> v;
v.push_back(10);
v.push_back(20);
std::cout << v[0] << ' ' << v[1] << '\n';
}
The class template SimpleAllocator declares value_type as a type alias for T. The member allocate takes a count n and requests from the global ::operator new enough bytes to contain n objects of type T. It then casts the returned void pointer to a T* and returns it. The member deallocate takes the pointer that was returned previously from allocate and passes it to the global ::operator delete, ignoring the second parameter, the size, in this simple implementation.
The vector is initialized as std::vector<int, SimpleAllocator<int>>.
Whenever the vector requires space for its elements, it invokes SimpleAllocator::allocate. When the vector is destructed or reduced in size, it invokes SimpleAllocator::deallocate.
The two push_back calls are pushing the values 10 and 20 respectively, and the final print statement outputs them.
Since the allocator is so thin, it behaves exactly as the default allocator. The only difference is that the path to allocation is now yours to control. You can replace the bodies of the allocate and deallocate functions with any other approach you like: an arena, a pool, or a region of static storage, as long as the returned memory is appropriately sized and aligned for T.
One common mistake is to not remember that allocate may throw. If the call to ::operator new fails to retrieve memory, it throws std::bad_alloc. The allocator must not catch this exception, and it must pass it on. Catching it would leave the container in an inconsistent state, and then the container would have undefined behavior.
An additional constraint to mention is that deallocate has to be called with the same pointer that was returned by allocate on the same allocator type, along with the corresponding size. Any other pointer or size results in undefined behavior.
Custom allocators now allow complete control of the location of a container's elements. The interface is designed to be minimal: provide an allocate and deallocate, ensure correct typing and alignment of memory, and the container takes over from there.
0 comments
No comments yet. Be the first.