std::allocator
std::allocator
是标准库提供的默认分配器模板,如果容器中没有显式指明分配器的话,就会使用这个分配器模板的实例为容器申请和释放内存。
std::allocator
的使用方式也很简单:
1 2 3 4 5 6 7 8 9
| allocator<int> alloc; int *numptr = alloc.allocate(2); cout << *numptr << endl; cout << *(numptr + 1) << endl; *numptr = 42; *(numptr + 1) = 13; cout << *numptr << endl; cout << *(numptr + 1) << endl; alloc.deallocate(numptr, 2);
|
这里实例化了一个int类型的分配器alloc,该实例对象只有两个成员函数allocate
和deallocate
,分别对应申请内存和释放内存(在C++17之前,std::allocator还有两个成员函数construct
和destory
,分别用于在申请到的内存上构造和析构对应类型的对象,C++17后弃用,并在C++20后彻底删除,并将这部分功能转移到了allocator_traits
中)。
在调用了allocate
方法后,分配器申请到了两个int大小的内存,并以指针的形式传递出去,此时这块内存是未进行初始化的,然后通过传递出来的指针,可以对申请到的内存进行操作,比如为其赋值。
然后在使用完成后,调用deallocate
方法释放掉分配器分配的内存。deallocate
需要传入指向内存的指针和对应的size,在默认分配器中,由于是直接使用的delete
,size并不影响内存释放,但如果使用的是自定义分配器,这样可以获得更大的灵活性。
在std::allocator
中定义了一些类型特性,如果使用自定义的分配器,为了兼容标准库的接口,应该定义相同的类型特性,或者直接继承std::allocator
模板类,这些类型特性包括:
1 2 3 4 5
| allocator<int>::difference_type; allocator<int>::size_type; allocator<int>::value_type; allocator<int>::propagate_on_container_move_assignment; allocator<int>::is_always_equal;
|
std::allocator_traits
allocator_traits
是分配器的萃取类型,其为所有符合标准的分配器提供了一致的接口,并为指定分配器中未实现的特性提供默认的实现。
1 2 3 4 5 6 7 8 9 10
| allocator<double> alloc2; allocator_traits<allocator<double>> alloc_t; double* dbptr = alloc_t.allocate(alloc2, 2); alloc_t.construct(alloc2, dbptr, 10); alloc_t.construct(alloc2, dbptr + 1, 20); cout << *dbptr << endl; cout << *(dbptr + 1) << endl; alloc_t.destroy(alloc2, dbptr + 1); alloc_t.destroy(alloc2, dbptr); alloc_t.deallocate(alloc2, dbptr, 2);
|
allocator_traits
实例化时指定包装的分配器类型,然后在所有的静态方法的第一个入参指定对应分配器的实例对象。allocator_traits
在分配器的基础上提供了construct
和destory
方法,用于构造和析构对应的元素和类型。
allocator_traits
同样可以用于自定义类型的分配器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class CustomC { public: CustomC() { cout << "default construct custom object\n"; } CustomC(int num) : num(num) { cout << "construct custom object\n"; } ~CustomC() { cout << "destory custom object\n"; }
int num{-1}; };
allocator<CustomC> alloc3; allocator_traits<allocator<CustomC>> alloc_t2; CustomC *custom_ptr = alloc_t2.allocate(alloc3, 2); alloc_t2.construct(alloc3, custom_ptr); alloc_t2.construct(alloc3, custom_ptr + 1, 100); cout << custom_ptr->num << endl; cout << (custom_ptr + 1)->num << endl; alloc_t2.destroy(alloc3, custom_ptr); alloc_t2.destroy(alloc3, custom_ptr + 1); alloc_t2.deallocate(alloc3, custom_ptr, 2);
|
allocator_traits
对于标准库默认的分配器有一个特化实现,但我们也完全可以使用自定义的分配器:
1 2 3 4 5 6 7 8 9 10 11
| template<typename T> class MyAlloc : public std::allocator<T> { };
MyAlloc<CustomC> myalloc; allocator_traits<MyAlloc<CustomC>> alloc_t3; CustomC* mycustom = alloc_t3.allocate(myalloc, 2); alloc_t3.construct(myalloc, mycustom); alloc_t3.construct(myalloc, mycustom + 1, 33); alloc_t3.destroy(myalloc, mycustom + 1); alloc_t3.destroy(myalloc, mycustom); alloc_t3.deallocate(myalloc, mycustom, 2);
|
这与上述默认的分配器执行结果相同,但会匹配allocator_traits
更一般的模板,并且由于我们没有实现construct
和destory
函数,执行时会转而使用allocator_traits
默认的方法。这正体现了allocator_traits
优势——只需要实现关心的部分,其他无关的部分标准库会帮你实现。
在容器中使用分配器
为了让容器可以更灵活地使用分配器,分配器萃取类allocator_traits
提供了rebind_traits
和rebind_alloc
类型特征(代替并简化了了C++11中分配器的rebind操作),通过这两个类型特征,可以生成一个与模板参数类型不同,但分配策略相同的分配器和萃取类。
从int类型的分配器中获取一个double类型的分配器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| MyAlloc<int> myialloc; allocator_traits<MyAlloc<int>> myialloc_trait; using my_rebind_alloc_type = allocator_traits<MyAlloc<int>>::rebind_alloc<double>; using my_rebind_alloc_trait = allocator_traits<MyAlloc<int>>::rebind_traits<double>; my_rebind_alloc_type mydballoc; my_rebind_alloc_trait my_dballoc_trait; double* dbptr = my_dballoc_trait.allocate(mydballoc, 2); my_dballoc_trait.construct(mydballoc, dbptr, 1.3); my_dballoc_trait.construct(mydballoc, dbptr + 1, 4.2); cout << *dbptr << endl; cout << *(dbptr + 1) << endl; my_dballoc_trait.destroy(mydballoc, dbptr); my_dballoc_trait.destroy(mydballoc, dbptr + 1); my_dballoc_trait.deallocate(mydballoc, dbptr, 2);
|
用途在于容器为了方便管理,往往会对元素类型再次封装,而模板参数传入的分配器是元素类型的,通过重新绑定到容器内部封装的类型,保证分配策略的一致性。
最后演示一个自定义的链表容器和插入操作实现,链表容器内部通过Node类型管理插入的元素:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| template <typename T, typename Alloc = std::allocator<T>> class List { public: using value_type = T; using alloc_trait = typename std::allocator_traits<Alloc>; using node_alloc_type = typename alloc_trait::template rebind_alloc<Node>; using rebind_trait = typename alloc_trait::template rebind_traits<Node>;
struct Node { value_type data{}; Node* next{nullptr}; };
List() = default;
void insert(T elem) { rebind_trait rebind_t; Node* node = rebind_t.allocate(m_alloc, 1); rebind_t.construct(m_alloc, node); node->data = elem; return insert(m_root, node); }
private: void insert(Node* root, Node* node) { if(m_root == nullptr) { m_root = node; return; } if(m_root->next == nullptr) { m_root->next = node; return; } return insert(m_root->next, node); }
public: Node* m_root{nullptr}; node_alloc_type m_alloc{}; };
|
在列表容器内部,保存了容器元素类型的分配器,但在容器内部元素是通过Node结构来封装的。所以每次插入元素时,我们都将分配器重新绑定一个模板参数为Node的分配器,其使用的分配策略和指定传给容器的分配器相同,只是分配的类型不同。
1 2 3 4 5
| List<int> mylist; mylist.insert(10); mylist.insert(20); cout << mylist.m_root->data << endl; cout << mylist.m_root->next->data << endl;
|