C++17 introduced polymorphic allocators, but what problem do they solve?
Polymorphic Allocators in C++
Standard Library Containers use Allocators
Containers in C++ are typically defined in terms of their type, supplied as a template parameter. For
example, here’s a vector of int
.
std::vector<int> vectorOfInt = {1, 2, 3, 5, 7, 11, 13, 17, 19, 23};
However, if you look at the definition for std::vector
then you’ll see that there are actually two template parameters. The first template parameter is the
type of the object stored in the container, and the second is an Allocator for it. This second
template parameter defaults to std::allocator<T>
.
Standard library containers use allocators to allocate and deallocate memory. In fact, nearly all standard library components that need to allocate or deallocate memory use an allocator to do so.
Here’s a vector of int
that uses std::allocator<int>
explicitly.
std::vector<int, std::allocator<int>> vectorOfIntStdAllocator;
You can assign between these vectors because they’re the same type. In other words,
std::vector<int>
is the same type as std::vector<int, std::allocator<int>>
.
// Same type, so assignment is possible.
vectorOfIntStdAllocator = vectorOfInt;
vectorOfInt = vectorOfIntStdAllocator;
Implementing an Allocator
By its nature, std::allocator
is general purpose and therefore has to cater for many scenarios. It
has no idea if your application needs a large number of similarly sized objects, or objects of a
wide variety of sizes. It has no idea if they’ll be short-lived or long-lived, or if they’ll be
large or small.
It won’t be as efficient as a custom allocator because it has no way of knowing about the memory usage of your application.
Fortunately, the containers in the C++ standard library allow you to provide your own custom allocators that are more suited to your needs. For example, if you’re handling something short-lived, such as a web request in which none of the objects will be needed once the response has been sent, then you could use a bump allocator that never deallocates, then frees memory in one operation at the end, once the request has been handled.
To be an allocator, a class has to meet certain named requirements.
The named requirements for Allocator
are defined on this page in cppreference.com.
A minimal C++11 allocator
Here’s a minimal C++11 allocator, based on the example from that page. It is written in terms of
malloc()
and free()
, and logs allocations and deallocations to stdout.
template<class T>
struct ChattyAllocator
{
using value_type = T;
ChattyAllocator() = default;
template<class U>
constexpr ChattyAllocator(const ChattyAllocator<U>&) noexcept
{
}
[[nodiscard]] T* allocate(std::size_t n)
{
if (n > std::numeric_limits<std::size_t>::max() / sizeof(T))
{
throw std::bad_array_new_length();
}
if (auto p = static_cast<T*>(std::malloc(n * sizeof(T))))
{
std::cout << "Allocated " << n * sizeof(T) << " bytes at 0x" << std::hex
<< reinterpret_cast<void*>(p) << std::dec << '\n';
return p;
}
throw std::bad_alloc();
}
void deallocate(T* p, std::size_t n) noexcept
{
std::cout << "Deallocated " << n * sizeof(T) << " bytes at 0x" << std::hex
<< reinterpret_cast<void*>(p) << std::dec << '\n';
std::free(p);
}
};
template<class T, class U>
bool operator==(const ChattyAllocator<T>&, const ChattyAllocator<U>&)
{
return true;
}
template<class T, class U>
bool operator!=(const ChattyAllocator<T>&, const ChattyAllocator<U>&)
{
return false;
}
Here’s a vector of int
that uses the ChattyAllocator
.
std::vector<int, ChattyAllocator<int>> vectorWithCustomAllocator;
Unfortunately, using a different allocator changes the type of the vector. The vectors up
to this point have been std::vector<int, std::allocator<int>>
, but the new one is
std::vector<int, ChattyAllocator<int>>
.
Assignment between these vectors is not allowed, because they’re different types.
// Assigning from std::vector<int, ChattyAllocator<int>> to std::vector<int> is not allowed.
vectorOfInt = vectorWithCustomAllocator;
// Assigning from std::vector<int> to std::vector<int, ChattyAllocator<int>> is not allowed.
vectorWithCustomAllocator = vectorOfInt;
Polymorphic Allocators and Memory Resources
To solve this problem, C++17 introduced polymorphic allocators,
in which allocators use memory resources
to perform allocation and deallocations on their behalf. Polymorphic allocators and memory
resources come from the <memory_resource>
header.
Here’s a vector of int
that uses a polymorphic allocator.
std::pmr::vector<int> vectorOfIntPmr = {29, 31, 37, 41, 43, 47, 53, 59, 61, 67};
Note that std::pmr::vector<int>
is an alias for std::vector<int, std::pmr::polymorphic_allocator<int>>
.
std::vector<int, std::pmr::polymorphic_allocator<int>> vectorOfIntPmrAllocator;
You can assign between these vectors because they’re the same type. In other words,
std::pmr::vector<int>
is the same type as std::vector<int, std::pmr::polymorphic_allocator<int>>
.
vectorOfIntPmrAllocator = vectorOfIntPmr;
vectorOfIntPmr = vectorOfIntPmrAllocator;
You might think that there’s nothing new here as this was also possible with std::vector<int>
and
std::vector<int, std::allocator<int>>
. However, polymorphic allocators, in combination with memory
resources, allow for assignment between allocators that use different allocation strategies.
Memory resources derive from std::pmr::memory_resource
, and look very much like allocators, in
that they have allocate()
and deallocate()
methods. They are implemented by overriding the pure
virtual methods do_allocate()
, do_deallocate()
, and do_is_equal()
.
A minimal memory resource
Here’s a minimal example of a memory resource. It uses
std::pmr::new_delete_resource()
to
allocate and deallocate memory. As with ChattyAllocator
, it also logs allocations and
deallocations to stdout.
class ChattyMemoryResource : public std::pmr::memory_resource
{
void* do_allocate(std::size_t n, std::size_t alignment) override
{
void* p = std::pmr::new_delete_resource()->allocate(n, alignment);
std::cout << "Allocated " << n << " bytes at 0x" << std::hex << reinterpret_cast<void*>(p)
<< std::dec << '\n';
return p;
}
void do_deallocate(void* p, std::size_t n, std::size_t alignment) override
{
std::cout << "Deallocated " << n << " bytes at 0x" << std::hex << reinterpret_cast<void*>(p)
<< std::dec << '\n';
return std::pmr::new_delete_resource()->deallocate(p, n, alignment);
}
bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override
{
return std::pmr::new_delete_resource()->is_equal(other);
}
};
Here’s a polymorphic allocator that uses it.
ChattyMemoryResource chattyMemoryResource;
std::pmr::polymorphic_allocator<int> chattyPolymorphicAllocator(&chattyMemoryResource);
Here’s a vector of int
that uses the new polymorphic allocator.
std::pmr::vector<int> vectorOfIntChattyPolymorphicAllocator{chattyPolymorphicAllocator};
You can assign between vectors that use polymorphic allocators, even when those allocators use different memory resources.
vectorOfIntChattyPolymorphicAllocator = vectorOfIntPmr;
vectorOfIntPmr = vectorOfIntChattyPolymorphicAllocator;
And that’s the problem solved by polymorphic allocators.
Gotchas
There are, of course, caveats. The notes section for polymorphic allocators on cppreference.com gives a warning that polymorphic allocator does not propagate on container copy assignment, move assignment or swap, and goes on to explain the implications.
Further Reading
For more information, follow these links.
- The Dumbest Allocator of All Time in which vector-of-bool implements a bump allocator.
- Thanks for the memory (allocator) in which Glennan Carnie of Feabhas Ltd gives a great breakdown of polymorphic allocators and memory resources.
- Dynamic Memory Management on cppreference.com
- Memory Resource on cppreference.com
- Polymorphic Allocator on cppreference.com.