带有自定义分配器/本地分配器的std::unordered_map不编译

std::unordered_map with custom allocator / local allocator does not compile

本文关键字:分配器 unordered 编译 map std 自定义      更新时间:2024-09-22

我有一个非常简单的自定义/本地分配器。我的目标是使用堆栈上的数组作为内存的分配部分。它似乎在std::vector中工作,但当我尝试将它插入std::unordered_map时,它无法编译。gcc 7.4.0的错误消息非常难以理解。大致如下:

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更易于管理。从类似error: no matching conversion for functional-style cast from 'const std::_Hashtable . . .的错误中滚动我发现:

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;
^

让它更清楚地表明这个std::__detail::_Hash_node_base位正在妨碍它。以下是代码,两个unordered_map声明都没有编译:

#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;
}

T类型的分配器必须可绑定到U类型的分配器——这就是为什么有rebind模板的原因。

要做到这一点,您必须提供一种将构造从类型U转换为类型T的方法,例如,从MonotonicIncreasingAllocator<U, ...>&构造的构造函数,例如:

template <typename U>
MonotonicIncreasingAllocator( const MonotonicIncreasingAllocator<U, max_size>& )

您可能会立即注意到一个问题:array<U,max_size>不一定要复制到array<T,max_size>;因此,您需要重新思考分配器的设计[1]

由于遗留的原因;分配器";该模型旨在具有可复制性。这种要求使得很难使用本身包含状态而不是间接指向状态的分配器。

注意:这可能对vector有效的原因是,类型为T的分配器不会在vector<T>上反弹,因为它只需要分配Tn实例。对于更复杂的数据结构,如mapsetunordered_map等,情况并非如此,因为可能存在对象的节点或内部使用的其他连续序列。


[1]有状态分配器直接存储在使用它们的容器中。这意味着vector<T,MonotonicIncreasingAllocator<T,N>>现在还将直接在vector类内部存储分配器本身,其中包含array<T,N>,以及它自己的数据——这是浪费。使用此分配器复制甚至移动容器将是一项极其昂贵的操作。

此外,通过将数据直接存储在分配器内部,转换构造需要整个内部std::array对象的副本,这意味着重新绑定构造了一个对象,该对象引用了与正在反弹的分配器不同的单调结构,这并不理想。

您应该查看std::pmr::polymorphic_allocator中使用的体系结构以获得更好的灵感。std::pmr::polymorphic_allocator保留1种数据类型:std::memory_resource指针,这使得重新绑定变得便宜,并且该分配器的存储也很便宜。memory_resource是类型不明确的,通过间接传递,这允许分配器在反弹后使用和引用同一内存池。

正如@Human Compiler在回答中所说,不应该将分配的数据与分配器耦合。解决方案相当简单:从堆栈上所需的数组中传入指针。您不必为在这个线程和其他地方发现的所有分配器包装器的废话而烦恼。在SOLID术语中,数据作为依赖项注入分配器。

我仍然觉得重新绑定界面非常奇怪。这显然是我们一直坚持的糟糕设计。除了编写struct rebind { other...陈旧别名之外,还必须提供反弹类型的复制构造函数。后者几乎没有记录,如果有的话。

#include <array>
#include <unordered_map>
#include <vector>
struct SharedArray 
{
uint8_t* data;
uint64_t index;
};
template<class T>
class MonotonicIncreasingAllocator
{
public:
MonotonicIncreasingAllocator(SharedArray& a) : _data{a} {}
template<class U>
MonotonicIncreasingAllocator(const MonotonicIncreasingAllocator<U>& rhs)
: _data{const_cast<MonotonicIncreasingAllocator<U>&>(rhs).data()} {}
using type = MonotonicIncreasingAllocator<T>;
using other = MonotonicIncreasingAllocator<T>;
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>;
T* allocate(std::size_t n)
{
T* r = _data.data + _data.index;
_data.index += n * sizeof(T);
return r;
}
constexpr void deallocate(T* p, std::size_t n)
{
return;
}
SharedArray& data()
{
return _data;
}

private:
SharedArray& _data;
};
int main()
{
using namespace std;
using key = int;
using value = string;
using item = pair<key, value>;
std::array<uint8_t, 4096> arr; // allocate enough, here but a page
SharedArray sharr;
sharr.index = 0;
sharr.data = arr.begin();
using alloc = MonotonicIncreasingAllocator<item>;
alloc a0(sharr);
alloc a1(sharr);
vector<item, alloc> v0(a0);
unordered_map<key, value, hash<key>, equal_to<key>, alloc> m(500, a1);
return 0;
}