首先什么是Allocator?Allocator有什么用?
分配器是负责封装堆内存管理的对象,它们在整个标准库中使用,特别是STL容器使用它们来管理r容器内部的所有内存分配,大部份情况下,程序员不用理会,标准容器使用默认的分配器称为std :: allocator,例如当你声明一个简单的vector对象时,C++编译器默认已经使用了内置的std::allocator,在标准库的vector模板当中,第二个模板参数_Alloc就是std::allocator,实际上,std::allocator也是一个类模板
int main(){
std::vector<int> a;
}
先简要回顾一下历史。Alexander Stepanov(STL之父)提出了分配器的想法,其动机是使容器完全独立于底层内存模型。他打算让分配器完全封装内存模型,但标准委员会发现这很危险,因为这种方法会导致不可接受的效率下降。因此,分配器的当前用途是让程序员控制容器内的内存分配,而不是采用底层硬件的地址模型
本篇从零实现自己的内存分配器,来理解std::allocator的内部运行机制,如果你认为这是重造轮子的话,送你一个字“滚”,自定义allocator有很多现实的原因。
- 有些嵌入式平台没有提供默认的malloc/free等底层内存管理函数,你需要继承std::allocator,并封装自定义版本的malloc/free等更底层的堆内存管理函数。
- 使用C++实现自己的数据结构,有时我们需要扩展(继承)std::allocator。
- 大部分用C++写的游戏程序都有自己重新实现的allocator。
每个容器实例中都有一个Allocator实例。它向分配器请求存储来存储元素。分配器应具备的基本成员函数如下:
- T*allocate(size_t n);分配足够的存储空间来存储T的n个实例,并返回指向它的指针
- void deallocate(T* p, size_t n) 释放分配的内存
- void construct(T* p, Args ... args);使用p指向的args参数构造一个对象,该接口在C++20中已被移除
- void destroy(T* p);调用p指向的对象的析构函数,该接口在C++20中已被移除
标准库中的allocator还有一些可选的成员,视不同C++编译器版本而异。
- 指向类型T的指针,以及指向类型为T的常量指针
- 类型T的引用,以及类型T的常量引用
- 自身类型T的别名value_type
- 能表达allocation模型中最大对象大小的一个无符号整数类型,并且表示在allocation模型中两个指针区别(difference_type)的有符号整数
- size_type max_size()返回有关类型T的分配内存的最大值。该接口在C++20中已被移除
MyAllocator实现
我们下面从0实现一个符合定义的allocator,首先我们要在调用层代码,在声明一个vector实例,显式传入我们自定义的MyAllocator,并使用他来分配内存
int main(void){
std::vector<int,MyAllocator<int>> v;
}
下面是我们对MyAllocator的定义,并且定义了标准库规定的所有类型别名,下面的类型别名在标准库的std::allocator_traints中都提供了默认值,因此这里列出所有类型别名仅仅为了完整说明一个allocator的实现过程。
template<class T>
class MyAllocator{
public:
using value_type T;
using pointer=T*;
using const_pointer=const T*;
using void_pointer=void*;
using const_void_pointer=const void*;
using size_type size_t;
using difference_type=std::ptrdiff_t
MyAllocator()=default;
~MyAllocator()=default;
};
allocator的堆内存管理接口
我们跟着下来需要实现的是allocate成员函数和dealloc成员函数,整个allocator的堆内存管理都围绕这两个接口展开。我们需要对C++的allocator的堆内存接口调用顺序有个清晰的认识,如下图所示。
我们在MyAllocator实现的allocate和deallocate函数是必须的,
template<class T>
class MyAllocator{
public:
.....
pointer allocate(size_type numObjs){
return static_cast<pointer>(operator new(sizeof(T)*numObjs));
}
pointer allocate(size_type numObjs,const_void_pointer hit){
return allocate(numObjs);
}
void deallocate(pointer p,size_type numObjs){
operator delete(p);
}
}
我们这里也定义了另一个allocate版本的成员函数,接受一个numObjs参数,并接受一个已分配堆内存的指针,他是一个只想最近分配的元素的指针。可以使用他来改进已分配内存的释放,只是为了提供缓存性能,在我们的示例中,我们会忽略它并立即返回。
其他的成员函数实现
size_type max_size() const{
return std::numeric_limits<size_type>::max();
}
对象构造的问题
这是C++20中,被丢弃的内容,但为了完整说明,我在本篇中仍然会提及该内容。
C++20之前的标准库,在allocator中,我们有两个的方法用于构造和销毁对象,一种叫construct,用于在我们分配的内存中构造类型T的实际对象。需要注意的是,当我们调用new操作符分配了原始内存,new操作符并不会执行任何类型T的初始化.
我们看看构造函数是如何工作的,首先,对于allocator下的construct方法,它也是一个类模板的成员函数,如下代码定义,该成员函数接受一个类型U的指针p(已分配了堆内存),construct的运行原理非常简单,他在construct函数内部原地调用类型U的构造函数,并且转发外部的任意参数传递给类型U的构造函数
template<class T>
class MyAllocator{
public:
//类型别名的代码省略
//其余代码略
//构造函数
template<class U,class...Args>
void construct(U *p,Args &&...){
new(p) U(std::forward<Args>(args)...);
}
}
同理的destroy接口实现如下
template<class T>
class MyAllocator{
public:
//类型别名的代码省略
//其余代码略
//构造函数
template<class U,class...Args>
void destroy(U *p){
p->~U();
}
}
这两个函数默认情况下是完全可选,我这里只是展示如何完整的MyAllocator的自定义实现。
内置绑定机制
上面实现的必选项和可选项的allocator的类成员,都是标准库已经规定的接口名称,没什么好说的。而allocator的难点就是rebind这个内部类成员,而且它也是一个类模板。
rebind内部类成员
根据标准库的定义,rebind被定义为std::allocator类的结构成员; 此结构定义了other成员,该成员定义为专门用于不同参数类型的分配器的实例(other成员定义了可以创建不同类型的对象的分配器类)
class MyAllocator{
public:
.....
template<class U>
struct rebind{using other=MyAllocator<U>;}
}
std::MyAllocator模板用于获取某种类型T的对象。 容器内部可能需要分配不同类型的对象。 例如,当您有一个std :: list <T,MyAllocator>时,分配器MyAllocator用于分配类型T的对象,而std :: list <T,MyAllocator>实际上需要分配某个节点类型Node<T>的对象,从模板函数的角度来考虑,我们通常会用另为一个模板参数U来表示 调用节点类型Node<T>,std :: list <T,MyAllocator>需要获得类型U对象的分配器,该分配器使用MyAllocator提供的分配机制。对于程序员在定义自己的数据结构时,需要在自己实现的容器内部定义类似如下的语句
typename MyAllocator::template rebind<U>::other
指定相应的类型。 现在,此声明中有一些语法上令人困惑的地方:
- 由于rebind是MyAllocator的成员模板,而MyAllocator是std::list模板参数,因此rebind成为从属名称。 为了表明从属名称是模板,需要在其前面加上template前缀。 如果没有template关键字,则<将被视为小于运算符。
- other也取决于模板参数,即它也是一个从属名称。 为了表明从属名称是一种类型,需要typename关键字。
那么,我们在自定义的数据结构当中可能多次用到类U类型的allocator实例,那么我们可以在自定义的数据结构容器类的private作用域定义一个类型别名
typedef typename MyAllocator::template rebind<U>::other Elem_alloc_type
关于,MyAllocator说到这里应该是比较全面的了,他的全貌大概如下代码,若要确认该自定义的MyAllocator和std::allocator一样能够对所有std的容器起到作用,不妨在增加一些计数器的数据成员
#include <iostream>
#include <limits>
#include <stdlib.h>
template <class T>
class MyAllocator
{
public:
//下面是个类型别名,实现中的可选项
using value_type = T;
using pointer = T *;
using const_pointer = const T *;
using void_pointer = void *;
using const_void_pointer = const void *;
using size_type = size_t;
using difference = std::ptrdiff_t;
//重新绑定函数
template <class U>
struct rebind
{
using other = MyAllocator<U>;
};
MyAllocator() = default;
~MyAllocator() = default;
//分配内存
pointer allocate(size_type numObjects)
{
allocCount += numObjects;
std::cout << "MyAllocator::allocate,内存分配:" << numObjects << std::endl;
return static_cast<pointer>(operator new(sizeof(T) * numObjects));
}
//分配内存
pointer allocate(size_type numObjects, const_void_pointer hint)
{
return allocate(numObjects);
}
//释放内存
void deallocate(pointer p, size_type numObjects)
{
std::cout << "MyAllocator::deallocate,内存释放:" << numObjects << std::endl;
allocCount = allocCount - numObjects;
operator delete(p);
}
//分配器支持最大的内存数
size_type max_size() const
{
return std::numeric_limits<size_type>::max();
}
// //构造对象
// template <class U, class... Args>
// void construct(U *p, Args &&... args)
// {
// new (p) U(std::forward<Args>(args)...);
// }
// //销毁对象
// template <class U>
// void destroy(U *p)
// {
// p->~U();
// }
//返回每次分配/删除的内存数
size_type get_allocations() const
{
return allocCount;
}
private:
//统计当前内存的使用量
size_type allocCount;
我们的调用代码基本如下
#include "headers/Allocator.hh"
#include <cinttypes>
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <vector>
int main()
{
std::vector<int, MyAllocator<int>> v(0);
for (size_t i = 0; i < 30; i++)
{
sleep(1);
v.push_back(i);
std::cout << "当前容器内存占用量:" << v.get_allocator().get_allocations() << std::endl;
}
}
用vector执行下面的测试如下,附带小福利....
小结
这里没有生硬地罗列一堆std::allocator的api,而是采用原生实现的MyAllocator来说明std::allocator的内部机制。
- 分配器是STL容器的内存分配器。该容器可以将内存分配和取消分配与其元素的初始化和销毁分开。 因此,调用向量vec的vec.reserve(n)仅为至少n个元素分配内存。 每个元素的构造函数都不会执行。因为我们在最终的MyAllocator的construct接口和destoy接口取消了。
- 我们可以根据需要的容器来调整分配器,例如,您只希望vector内存resize操作的比例
- new不允许控制调用哪些构造函数,而只能同时构造所有对象。 这是std ::分配器优于new的优势。