本期主要讲解C++ STL中的内存分配器 std::allocator
及其特性萃取器__gnu_cxx::__alloc_traits
。
为防止混淆,规定如下:
-
allocator
:泛指内存分配器,仅仅是一个术语。 -
std::allocator
:是STL实现的内存分配器类std::allocator
。
__gnu_cxx::new_allocator
C++的默认的内存分配器std::allocator
,继承至__gnu_cxx::new_allocator
。而 __gnu_cxx::new_allocator
主要完成两个任务:
- 分配对象内存、初始化对象
- 析构对象、释放对象内存
__gnu_cxx::new_allocator
是个空类,没有成员变量,主要有四种成员函数完成上述任务:
-
allocate
函数,用于分配内存 -
construct
函数,调用已分配内存对象的构造函数 -
destroy
函数,调用析构函数 -
deallocate
函数,用于释放内存
__gnu_cxx::new_allocator
的整体框架大致如下:
template <typename _Tp>
class new_allocator
{
public:
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef _Tp* pointer;
typedef const _Tp* const_pointer;
typedef _Tp & reference;
typedef const _Tp & const_reference;
typedef _Tp value_type;
template <typename _Tp1>
struct rebind
{
typedef new_allocator<_Tp1> other;
};
new_allocator() _GLIBCXX_USE_NOEXCEPT {}
new_allocator(const new_allocator &) noexcept {}
template <typename _Tp1>
new_allocator(const new_allocator<_Tp1> &) noexcept {}
~new_allocator() noexcept {}
pointer allocate(size_type __n, const void * = static_cast<const void *>(0));
void deallocate(pointer __p, size_type);
size_type max_size() const noexcept;
template <typename _Up, typename... _Args>
void construct(_Up *__p, _Args &&...__args)
noexcept(noexcept(::new ((void *)__p)_Up(std::forward<_Args>(__args)...)));
template <typename _Up>
void destroy(_Up *__p) noexcept(noexcept(__p->~_Up()));
//...
};
allocate
allocate
函数,用于分配大小为__n
个字节内存,返回值是分配所得内存的地址。
- 如果待分配内存大小
__n
大于当前进程最大可用内存,那么就会抛出bad_alloc
异常。 - 再调用
operator new
来分配内存。operator new
对malloc
作了一层简单的封装,等效于malloc
- 将指向
operator new
的返回值类型转换为此次分配对象_Tp
的指针类型。
整个过程如下:
pointer allocate(size_type __n, const void * = static_cast<const void *>(0))
{
if (__n > this->max_size())
std::__throw_bad_alloc();
#if __cpp_aligned_new
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
std::align_val_t __al = std::align_val_t(alignof(_Tp));
return static_cast<_Tp *>(::operator new(__n * sizeof(_Tp), __al));
}
#endif
return static_cast<_Tp *>(::operator new(__n * sizeof(_Tp)));
}
deallocate
deallocate
函数,使用operator delete
来释放地址__p
指向的内存。
void deallocate(pointer __p, size_type)
{
#if __cpp_aligned_new
if (alignof(_Tp) > __STDCPP_DEFAULT_NEW_ALIGNMENT__)
{
::operator delete(__p, std::align_val_t(alignof(_Tp)));
return;
}
#endif
::operator delete(__p);
}
construct
上面的allocate
函数相当于malloc
函数,只是分配__n
个字节的内存,但是并未对这片内存进行初始化。对allocate
函数分配的内存__p
进行初始化的任务,交给construct
函数来完成。
constuct
函数,使用了new
表达式的另一种形式,叫做placement new
,使用方式如下注释:
template <typename _Up, typename... _Args>
void construct(_Up *__p, _Args &&...__args)
noexcept(noexcept(::new ((void *)__p)_Up(std::forward<_Args>(__args)...)))
{
// 表示在 地址 _p 上调用对象 _Up的构造函数
// 其中,__args是构造函数的参数
::new ((void *)__p) _Up(std::forward<_Args>(__args)...);
}
destroy
deallocate
函数,是完成了释放内存,但是在释放内存之前一般需要先调用对象的析构函数,完成相关的资源的释放、关闭操作。因此在destoy
函数中,直接调用了类型_Up
的析构函数。
template <typename _Up>
void destroy(_Up *__p) noexcept(noexcept(__p->~_Up()))
{
__p->~_Up();
}
std::allocator
类std::allocator
继承__gnu_cxx::new_allocator
。
template<typename _Tp>
using __allocator_base = __gnu_cxx::new_allocator<_Tp>;
template <typename _Tp>
class allocator : public __allocator_base<_Tp>
{
public:
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef _Tp* pointer;
typedef const _Tp* const_pointer;
typedef _Tp& reference;
typedef const _Tp& const_reference;
typedef _Tp value_type;
template <typename _Tp1>
struct rebind
{
typedef allocator<_Tp1> other;
};
allocator() noexcept {}
allocator(const allocator &__a) noexcept : __allocator_base<_Tp>(__a) {}
allocator &operator=(const allocator &) = default;
template <typename _Tp1>
allocator(const allocator<_Tp1> &) _GLIBCXX_NOTHROW
{ }
~allocator() _GLIBCXX_NOTHROW {}
//...
// Inherit everything else.
};
rebind
在__gnu_cxx::new_allocator
、std::allocator
中都有一个rebind
函数,其主要作用:获得类型_Tp1
的内存分配器allocator<_Tp1>
。
template <typename _Tp1>
struct rebind
{
typedef allocator<_Tp1> other;
};
这个函数在容器中被STL中被广泛使用。比如,在std::list<_Tp, std::allocator<_Tp>>
中,std::allocator
不仅要为_Tp
类型的对象分配内存,还要为存储_Tp
对象的节点list_node<_Tp>
分配内存。但是std::list<_Tp, std::allocator<_Tp>>
的类模板参数中只是传入了用于分配_Tp
类型的内存分配器std::allocator<_Tp>
,那么怎么获得list_node<_Tp>
类型的内存分配器呢?
答案就是依靠rebind
函数:allocator<_Tp>::rebind<list_node<_Tp>>::other
,获得的就是用于分配list_node<_Tp>
类型的内存分配器 allocator<list_node<_Tp>>
。
在list
中的实现如下:
template<typename _Tp, typename _Alloc>
class _List_base
{
protected:
// 用于分配 _Tp 类型的内存分配器: _Tp_alloc_type
// _Tp_alloc_type 实际上就是 std::allocator
typedef typename __gnu_cxx::__alloc_traits<_Alloc>::template rebind<_Tp>::other _Tp_alloc_type;
// 用于分配 List_node<_Tp> 类型的内存分配器:_Node_alloc_type
typedef typename _Tp_alloc_traits::template rebind<_List_node<_Tp> >::other _Node_alloc_type;
//...
};
template<typename _Tp, typename _Alloc = std::allocator<_Tp> >
class list : protected _List_base<_Tp, _Alloc>
{
protected:
typedef _List_node<_Tp> _Node;
//...
};
std::__allocator_traits_base
上面的list
中使用到了用于萃取内存分配器属性的类__gnu_cxx::__alloc_traits
。
__gnu_cxx::__alloc_traits 继承自 std::allocator_traits
std::allocator_traits 继承自 std::__allocator_traits_base
类__allocator_traits_base
,用于获取内存分配器_Alloc
的属性,这个分配器_Alloc
不一定是上面所述的std::allocator
,可以是自定义的。
struct __allocator_traits_base
{
template <typename _Tp,
typename _Up,
typename = void>
struct __rebind : __replace_first_arg<_Tp, _Up>
{ };
// __rebind 特化版本:当分配器 _Tp 有成员函数 rebind 时调用此特化版本
template <typename _Tp, typename _Up>
struct __rebind<_Tp,
_Up,
__void_t<typename _Tp::template rebind<_Up>::other>>
{
using type = typename _Tp::template rebind<_Up>::other;
};
protected:
template <typename _Tp> using __pointer = typename _Tp::pointer;
template <typename _Tp> using __c_pointer = typename _Tp::const_pointer;
template <typename _Tp> using __v_pointer = typename _Tp::void_pointer;
template <typename _Tp> using __cv_pointer = typename _Tp::const_void_pointer;
template <typename _Tp> using __pocca = typename _Tp::propagate_on_container_copy_assignment;
template <typename _Tp> using __pocma = typename _Tp::propagate_on_container_move_assignment;
template <typename _Tp> using __pocs = typename _Tp::propagate_on_container_swap;
template <typename _Tp> using __equal = typename _Tp::is_always_equal;
};
__rebind
类__allocator_traits_base
中最重要的是成员函数__rebind
。 __rebind
的模板参数_Tp
是分配器类型,根据_Tp
来实现重载:
-
当传入的内存分配器类型
_Tp
,实现了rebind
成员函数时,比如上面的std::allocator
,那么就调用__rebind
的特化版本:template <typename _Tp, typename _Up> struct __rebind<_Tp, _Up, __void_t<typename _Tp::template rebind<_Up>::other>> { using type = typename _Tp::template rebind<_Up>::other; };
以
std::allocator<int>
为例,获取Node<int>
类型的内存分配器:__allocator_traits_base::__rebind<std::allocator<int>, Node<int>>::type // 等价于 std::allocator<Node<int>>
-
当传入的分配器
_Tp
没有实现rebind
成员函数时,就调用普通__rebind
版本:// _Tp需要是分配器 template <typename _Tp, typename _Up, typename = void> struct __rebind : __replace_first_arg<_Tp, _Up> { };
其中
__replace_first_arg
实现如下。此时,需要自定义一个内存分配器模板_Template
,template <typename _Tp, typename _Up> struct __replace_first_arg { }; // _Template 是个类模板 template <template <typename, typename...> class _Template, typename _Up, typename _Tp, typename... _Types> struct __replace_first_arg<_Template<_Tp, _Types...>, _Up> { using type = _Template<_Up, _Types...>; };
by the way
在此,补充点模板的一点知识:
-
模板参数模板
在
__replace_first_arg
类中,使用了一个类模板参数模板_Template
,这表示模板参数_Template
本身就是个类模板。template <typename, typename...> class _Template
-
::template
在
__rebind
函数体中,在::
后面有个template
关键字,这是用于告诉编译器template
后面的<
不是比较符号,而是模板参数符号。就是类似于_Tp
前面的typename
是告诉编译器::
后面的是类成员函数,而不是static
函数。using type = typename _Tp::template rebind<_Up>::other;
__alloc_rebind
全局函数__alloc_rebind
,是std::__allocator_traits_base
的wrapper,用于获取为_Up
类型分配内存的内存分配器_Alloc<_Up>
template <typename _Alloc, typename _Up>
using __alloc_rebind = typename __allocator_traits_base::template __rebind<_Alloc, _Up>::type;
std::allocator_traits
类std::allocator_traits
,继承于std::__allocator_traits_base
,用于获取内存分配器allocator
的各个属性。
template <typename _Alloc>
struct allocator_traits : __allocator_traits_base
{
typedef _Alloc allocator_type; /// The allocator type
typedef typename _Alloc::value_type value_type; /// The allocated type
//...
};
当_Alloc
是std::allocator
时,有个特化版本:
template <typename _Tp>
struct allocator_traits<allocator<_Tp>>
{
using allocator_type = allocator<_Tp>; // 分配器类型
using value_type = _Tp; // 待分配内存的对象类型
using pointer = _Tp *; // 对象指针
using const_pointer = const _Tp *;
//... using
using is_always_equal = true_type;
// 使用allocator为_Up分配内存
template <typename _Up>
using rebind_alloc = allocator<_Up>;
template <typename _Up>
using rebind_traits = allocator_traits<allocator<_Up>>;
// 下面是 std::allocator<_Tp> 成员函数的 wrapper
static pointer allocate(allocator_type &__a, size_type __n)
{
return __a.allocate(__n);
}
static pointer allocate(allocator_type &__a, size_type __n, const_void_pointer __hint)
{
return __a.allocate(__n, __hint);
}
static void deallocate(allocator_type &__a, pointer __p, size_type __n)
{
__a.deallocate(__p, __n);
}
template <typename _Up, typename... _Args>
static void construct(allocator_type &__a, _Up *__p, _Args &&...__args)
noexcept(noexcept(__a.construct(__p, std::forward<_Args>(__args)...)))
{
__a.construct(__p, std::forward<_Args>(__args)...);
}
template <typename _Up>
static void destroy(allocator_type &__a, _Up *__p) noexcept(noexcept(__a.destroy(__p)))
{
__a.destroy(__p);
}
static size_type max_size(const allocator_type &__a) noexcept
{
return __a.max_size();
}
};
__gnu_cxx::__alloc_traits
__gnu_cxx::__alloc_traits
类,也大都是std::allocator_traits
的wrapper,
template<typename _Alloc, typename = typename _Alloc::value_type>
struct __alloc_traits : std::allocator_traits<_Alloc> {
typedef _Alloc allocator_type;
typedef std::allocator_traits<_Alloc> _Base_type;
typedef typename _Base_type::value_type value_type;
typedef typename _Base_type::pointer pointer;
typedef typename _Base_type::const_pointer const_pointer;
typedef typename _Base_type::size_type size_type;
typedef typename _Base_type::difference_type difference_type;
typedef value_type & reference;
typedef const value_type& const_reference;
using _Base_type::allocate;
using _Base_type::construct;
using _Base_type::deallocate;
using _Base_type::destroy;
using _Base_type::max_size;
private:
// 当 _Ptr 不是个标准指针,但是 _Ptr 和 value_type* 相同
// __is_custom_pointer 才是 true,即 _Ptr 是个自定义指针
// 即 _Ptr 可转换为 pointer
template <typename _Ptr>
using __is_custom_pointer
= std::__and_<std::is_same<pointer, _Ptr>, std::__not_<std::is_pointer<_Ptr>>>;
public:
// overload construct for non-standard pointer types
// 重载非标准类型的指针,调用构造函数
template <typename _Ptr, typename... _Args>
static typename std::enable_if<__is_custom_pointer<_Ptr>::value>::type
construct(_Alloc &__a, _Ptr __p, _Args &&...__args) noexcept(...) // 省略了noexcept中的表达式
{
// 使用分配器 __a , 在地址 __p 调用构造函数
_Base_type::construct(__a,
std::__to_address(__p),
std::forward<_Args>(__args)...);
}
// overload destroy for non-standard pointer types
// 重载非标准类型指针,调用析构函数
template <typename _Ptr>
static typename std::enable_if<__is_custom_pointer<_Ptr>::value>::type
destroy(_Alloc &__a, _Ptr __p) noexcept(...)
{
_Base_type::destroy(__a, std::__to_address(__p));
}
/*** 对于标准的指针,会直接调用父类的constuct、destroy ***/
// wrapper
template <typename _Tp>
struct rebind
{
typedef typename _Base_type::template rebind_alloc<_Tp> other;
};
//...
}
总体来说,__gnu_cxx::__alloc_traits
提供了一个顶层的内存分配器萃取器,可以使用 _Alloc
的 allocate
、 deallocate
、construct
以及 destroy
等函数来完成对象构造和析构等任务。
而类std::allocator_traits
是底层直接获取内存分配器_Alloc
属性的类,其中std::allocator_traits
有个特化版本,即使_Alloc
是std::allocator
,因为std::allocator
是STL的容器默认的内存分配器。
如果想将自定义的内存分配器Custome_Alloc
融入到STL体系中,那么也需要像std::allocator
一样完成相应的接口设计、以及rebind
函数。这样,容器就能通过__gnu_cxx::__alloc_traits<Custome_Alloc>
使用自定义的内存分配器Custome_Alloc
。
好嘞,到此完成了本期的目标,即讲解完毕C++ STL内存分配器的设计。