I have a pretty simple custom/local allocator. My goal is to use an array on the stack as the allocating portion of memory. It appears to work in std::vector
but when I try to plug it in to std::unordered_map
it fails to compile. gcc 7.4.0's error messages are pretty impenetrable. Something along the lines of:
hashtable_policy.h:2083:26: error: no matching function for call to
‘MonotonicIncreasingAllocator<std::pair<const int, std::string>, 500>::
MonotonicIncreasingAllocator(std::__detail::_Hashtable_alloc<MonotonicIncreasingAllocator
<std::__detail::_Hash_node<std::pair<const int, std::string>, false>, 500> >::
__node_alloc_type&)’
__value_alloc_type __a(_M_node_allocator());
Clang 7.1.0 is a bit more manageable. Scrolling from an error like error: no matching conversion for functional-style cast from 'const std::_Hashtable . . .
I find:
hashmap_custom_alloc.cpp:11:5: note: candidate constructor not viable: no known conversion from
'MonotonicIncreasingAllocator<std::__detail::_Hash_node<std::pair<const int,
std::__cxx11::basic_string<char> >, false>, [...]>' to 'const
MonotonicIncreasingAllocator<std::__detail::_Hash_node_base *, [...]>' for 1st argument
MonotonicIncreasingAllocator(const MonotonicIncreasingAllocator& rhs) = default;
^
Makes it a bit clearer this std::__detail::_Hash_node_base
bit is getting in the way. Here is the code, neither unordered_map declaration compiles:
#include <array>
#include <stdexcept>
#include <unordered_map>
#include <vector>
template<class T, std::size_t max_size>
class MonotonicIncreasingAllocator
{
public:
MonotonicIncreasingAllocator() : _index{0} {}
using type = MonotonicIncreasingAllocator<T, max_size>;
using other = MonotonicIncreasingAllocator<T, max_size>;
using value_type = T;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using propagate_on_container_move_assignment = std::true_type;
using is_always_equal = std::true_type;
template<class U>
using rebind = MonotonicIncreasingAllocator<U, max_size>;
T* allocate(std::size_t n)
{
T* r = _data.begin() + _index;
_index += n;
return r;
}
constexpr void deallocate(T* p, std::size_t n)
{
throw std::runtime_error("MontonicIncreasingAllocator can never deallocate()!");
}
private:
std::size_t _index;
std::array<T, max_size> _data;
};
int main()
{
using namespace std;
using key = int;
using value = string;
using item = pair<key, value>;
using alloc = MonotonicIncreasingAllocator<item, 500>;
alloc a0;
alloc a1;
vector<item, alloc> v0(a0);
vector<int, alloc> v1;
// unordered_map<key, value, hash<key>, equal_to<key>, alloc> m; // doesn't compile
// unordered_map<key, value, hash<key>, equal_to<key>, alloc> m(500, a1); // doesn't compile
return 0;
}
An allocator of type T
must be rebindable to an allocator of type U
-- this is why there is the rebind
template.
To do this you must offer a way to conversion-construct from a type U
to a type T
e.g. a constructor that constructs from MonotonicIncreasingAllocator<U, ...>&
, such as:
template <typename U>
MonotonicIncreasingAllocator( const MonotonicIncreasingAllocator<U, max_size>& )
You might notice a problem that immediately comes from this: an array<U,max_size>
cannot necessarily be copied to an array<T,max_size>
; and due to this, you will want to rethink your allocator design.[1]
For legacy reasons, the C++ "Allocator" model is meant to be copyable. This requirement makes it difficult to work with allocators that itself contain state, rather than indirectly point to state.
Note: The reason this may have worked for vector
is because an allocator of type T
doesn't get rebound on a vector<T>
, since it only needs to allocate n
instances of T
. This is not true for more complex data structures like a map
, set
, unordered_map
, etc -- since there may be nodes of objects or other contiguous sequences internally used.
[1] Stateful allocators are stored directly into the containers that use them. This means that a vector<T,MonotonicIncreasingAllocator<T,N>>
will now also store the allocator itself, containing an array<T,N>
, directly inside of the vector
class, in addition to its own data -- which is wasteful. Copying or even moving a container with this allocator would be an extremely expensive operation.
Additionally, by storing the data directly inside of the allocator, conversion-construction requires a copy of the entire internal std::array
object, which means that the rebinding constructs a new object that refers to a different monotonic structure than the allocator that was being rebound -- which isn't ideal.
You should look into the architecture that's used in std::pmr::polymorphic_allocator
for better inspiration. The std::pmr::polymorphic_allocator
holds onto 1 data type: a std::memory_resource
pointer, which makes rebinding cheap, and storage of this allocator cheap. The memory_resource
is type-ambiguous and passed by indirection, which allows for allocators after being rebound to use and refer to the same memory pool.