c++ 疑难杂症(13) allocator(c++ allocate)
在实践中曾经有个需求: 系统会陆续生产32字节固定大小串, 检查是否重复,不重复就添加, 最大的数据量可达到500万条记录,不使用磁盘,在内存中处理。
尝试了纯内存的sqlite, 占用内存太大了, 放弃;
也尝试直接使用std::map/std::set,都因为内存占用太大了,放弃;
最后是从std::map里把红黑树给抠出来, 使用整型及位域来减少节点变量大小, 一次性分配多个节点的内存之类的, 从而实现内存不超过230M, 满足了业务需求。
当时,如果是对std::allocator有所了解, 也许不用这么折腾了; 现在来学习学习。
1. 标准文档定义
std::allocator - cppreference.com
在标头 <memory> 定义 | |
template< class T > struct allocator; | |
template<> struct allocator<void>; | (C++17 中弃用) (C++20 中移除) |
如果不提供用户指定的分配器,那么 std::allocator 类模板是所有标准库容器使用的默认分配器。 默认分配器无状态,即给定分配器的任何实例都可交换、比较相等,且能由同一分配器类型的任何其他实例释放分配的内存。
对 void 的显式特化缺少成员类型定义(typedef)reference、const_reference、size_type 和 difference_type。 此特化不声明成员函数。 | (C++20 前) |
默认分配器满足分配器完整性要求。 | (C++17 起) |
成员类型
类型 | 定义 |
value_type | T |
pointer (C++17 中弃用) (C++20 中移除) | T* |
const_pointer (C++17 中弃用) (C++20 中移除) | const T* |
reference (C++17 中弃用) (C++20 中移除) | T& |
const_reference (C++17 中弃用) (C++20 中移除) | const T& |
size_type | std::size_t |
difference_type | std::ptrdiff_t |
propagate_on _container _move_assignment (C++11) | std::true_type |
rebind (C++17 中弃用) (C++20 中移除) | template< class U > struct rebind { typedef allocator<U> other; }; |
is_always_equal (C++11) (C++23 中弃用) (C++26 中移除) | std::true_type |
成员函数
(构造函数) | 创建新的分配器实例 (公开成员函数) |
(析构函数) | 析构分配器实例 (公开成员函数) |
address(C++20 前) | 获得对象的地址,即使重载了 operator& (公开成员函数) |
allocate | 分配未初始化的存储 (公开成员函数) |
allocate_at_least(C++23) | 分配与请求的大小至少一样大的未初始化存储 (公开成员函数) |
deallocate | 解分配存储 (公开成员函数) |
max_size(C++20 前) | 返回最大的受支持分配大小 (公开成员函数) |
construct(C++20 前) | 在分配的存储中构造对象 (公开成员函数) |
destroy(C++20 前) | 析构已分配存储中的对象 (公开成员函数) |
非成员函数
operator== operator!=(C++20 中移除) | 比较两个分配器实例 (公开成员函数) |
注解
成员模板 rebind 提供获得不同类型的分配器的方式。例如,std::list<T, A> 在分配某个内部类型 Node<T> 节点时会使用分配器A::rebind<Node<T>>::other (C++11 前)std::allocator_traits<A>::rebind_alloc<Node<T>>,它在 A 是 std::allocator 时以 A::rebind<Node<T>>::other (C++11 起) 实现。
成员类型 is_always_equal 由 LWG 问题 3170 弃用,因为它使得派生自 std::allocator 的定制分配器默认被当作始终相等。std::allocator_traitsstd::allocator<T>::is_always_equal 未被弃用,而它的成员常量 value 对任何 T 均为 true。
示例
#include <iostream>
#include <memory>
#include <string>
int main()
{
// int 的默认分配器
std::allocator<int> alloc1;
// 演示少见的直接使用成员
static_assert(std::is_same_v<int, decltype(alloc1)::value_type>);
int* p1 = alloc1.allocate(1); // 一个 int 的空间
alloc1.deallocate(p1, 1); // 而它没了
// 这些都可以通过特征使用,所以不需要直接使用
using traits_t1 = std::allocator_traits<decltype(alloc1)>; // 匹配的特征
p1 = traits_t1::allocate(alloc1, 1);
traits_t1::construct(alloc1, p1, 7); // 构造 int
std::cout << *p1 << '\n';
traits_t1::deallocate(alloc1, p1, 1); // 解分配 int 的空间
// string 的默认分配器
std::allocator<std::string> alloc2;
// 匹配的特征
using traits_t2 = std::allocator_traits<decltype(alloc2)>;
// 用 string 的特征重绑定产生同一类型
traits_t2::rebind_alloc<std::string> alloc_ = alloc2;
std::string* p2 = traits_t2::allocate(alloc2, 2); // 2 个 string 的空间
traits_t2::construct(alloc2, p2, "foo");
traits_t2::construct(alloc2, p2 + 1, "bar");
std::cout << p2[0] << ' ' << p2[1] << '\n';
traits_t2::destroy(alloc2, p2 + 1);
traits_t2::destroy(alloc2, p2);
traits_t2::deallocate(alloc2, p2, 2);
}
输出:
7
foo bar
构造函数
allocator() throw(); | (C++11 前) |
allocator() noexcept; | (C++11 起) (C++20 前) |
constexpr allocator() noexcept; | (C++20 起) |
allocator( const allocator& other ) throw(); | (C++11 前) |
allocator( const allocator& other ) noexcept; | (C++11 起) (C++20 前) |
constexpr allocator( const allocator& other ) noexcept; | (C++20 起) |
template< class U > allocator( const allocator<U>& other ) throw(); | (C++11 前) |
template< class U > allocator( const allocator<U>& other ) noexcept; | (C++11 起) (C++20 前) |
template< class U > constexpr allocator( const allocator<U>& other ) noexcept; | (C++20 起) |
构造默认分配器。因为默认分配器是无状态的,故构造函数无可见效应。
参数
other | 用以构造的另一 allocator |
2. 探索
2.1 自定义allocator 要求(c++11起, c++20前)
- 构造函数(参考上面的构造函数定义)
allocator() noexcept;
allocator( const allocator& other ) noexcept;
template< class U > allocator( const allocator<U>& other ) noexcept;
- 定义的成员类型(参考上面的成员类型)
value_type T
size_type std::size_t
difference_type std::ptrdiff_t
- 定义成员函数(参考上面的成员类型)
allocate
deallocate
address(非必要)
max_size(非必要)
construct(非必要)
destroy(非必要)
2.2 自定义allocator 示例
- 内部使用std::allocator方式
#include <iostream>
#include <vector>
template <typename _Ty>
class Allocator1 {
public:
//成员类型
using value_type = _Ty;
//using size_type = size_t;
//using difference_type = ptrdiff_t;
//构造函数
constexpr Allocator1() noexcept {}
constexpr Allocator1(const Allocator1&) noexcept = default;
template <class _Other>
constexpr Allocator1(const Allocator1<_Other>&) noexcept {}
//成员函数
_Ty* allocate(const size_t _Count) {
return alloc.allocate(_Count);
}
void deallocate(_Ty* const _Ptr, const size_t _Count) {
return alloc.deallocate(_Ptr, _Count);
}
#if 0
//非必要实现
template <class _Objty, class... _Types>
void construct(_Objty* const _Ptr, _Types&&... _Args) {
return alloc.construct<_Objty, _Types ...>(_Ptr, std::forward<_Types>(_Args) ... );
}
template <class _Uty>
void destroy(_Uty* const _Ptr) {
alloc.destroy<_Uty>(_Ptr);
}
size_t max_size() const noexcept {
return alloc.max_size();
}
template <class _Other>
struct rebind {
using other = Allocator1<_Other>;
};
_Ty* address(_Ty& _Val) const noexcept {
return alloc.address(_Val);
}
const _Ty* address(const _Ty& _Val) const noexcept {
return alloc.address(_Val);
}
#endif
private:
std::allocator<_Ty> alloc;
};
int main() {
class A {
int val = 0;
public:
A(int x) : val(x){
std::cout << "A(" << val << ")" << std::endl;
}
~A() {
std::cout << "~A(" << val << ")" << std::endl;
}
};
std::vector<A, Allocator1<A>> vec;
for (int i = 0; i < 100; i++) {
vec.emplace_back(i);
}
auto alloc = vec.get_allocator();
auto x = alloc.allocate(1);
//alloc.construct(x, 999);
new (x) A(999);
x->~A();
alloc.deallocate(x, 1);
return 0;
}
- 继承 std::allocator 方式
#include <iostream>
#include <vector>
#include <map>
template <typename _Ty>
class Allocator2 : public std::allocator<_Ty> {
//class Allocator2 : private std::allocator<_Ty> {
public:
//使用父类构造函数
using std::allocator<_Ty>::allocator;
using std::allocator<_Ty>::value_type;
//成员函数
_Ty* allocate(const size_t _Count) {
return std::allocator<_Ty>::allocate(_Count);
}
void deallocate(_Ty* const _Ptr, const size_t _Count) {
return std::allocator<_Ty>::deallocate(_Ptr, _Count);
}
};
int main() {
class A {
int val = 0;
public:
A(int x) : val(x) {
std::cout << "A(" << val << ")" << std::endl;
}
~A() {
std::cout << "A(" << val << ")" << std::endl;
}
};
std::vector<A, Allocator2<A>> vec;
for (int i = 0; i < 100; i++) {
vec.emplace_back(i);
}
//vec.get_allocator(); linux出错
std::map<int, int, std::less<int>, Allocator2<std::pair<const int, int>>> map;
map.insert({1, 1});
//map.get_allocator(); linux出错
return 0;
}
- 不使用std::allocator方式
#include <iostream>
#include <vector>
#include <map>
template <typename _Ty>
class Allocator3 {
public:
//成员类型
using value_type = _Ty;
//构造函数
constexpr Allocator3() noexcept {}
constexpr Allocator3(const Allocator3& x) noexcept = default;
template <class _Other>
constexpr Allocator3(const Allocator3<_Other>& x) noexcept {
totalSize = x.totalSize;
}
//成员函数
_Ty* allocate(const size_t _Count) {
size_t size = _Count * sizeof(_Ty);
totalSize += size;
return (_Ty*)std::malloc(size);
}
void deallocate(_Ty* const _Ptr, const size_t _Count) {
std::free(_Ptr);
}
void Show() {
std::cout << "分配大小: " << totalSize << std::endl;
}
size_t totalSize = 0;
};
int main() {
class A {
int val = 0;
public:
A(int x) : val(x) {
std::cout << "A(" << val << ")" << std::endl;
}
~A() {
std::cout << "A(" << val << ")" << std::endl;
}
};
std::vector<A, Allocator3<A>> vec;
for (int i = 0; i < 100; i++) {
vec.emplace_back(i);
}
vec.get_allocator().Show();
std::map<int, int, std::less<int>, Allocator3<std::pair<const int, int>>> map;
map.insert({ 1, 1 });
map.get_allocator().Show();
return 0;
}
3. 总结
通过上面的学习, 对allocator有了一定的了解, 以后碰上了, 不至于一头雾水。
有兴趣可以测试下 std::unordered_map 与 std::map 空间占比。
c++ 疑难杂症(11) std::forward_list