源码剖析STL内存分配器 std::allocator,让你能硬刚面试官

本期主要讲解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 
  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 
    struct rebind
    {
      typedef new_allocator<_Tp1> other;
    };

    new_allocator() _GLIBCXX_USE_NOEXCEPT {}
    new_allocator(const new_allocator &) noexcept {}
    template 
    new_allocator(const new_allocator<_Tp1> &) noexcept {}
    ~new_allocator() noexcept {}
 
    pointer allocate(size_type __n, const void * = static_cast(0));
      
    void deallocate(pointer __p, size_type);
      
    size_type max_size() const noexcept;

    template 
    void construct(_Up *__p, _Args &&...__args) 
                    noexcept(noexcept(::new ((void *)__p)_Up(std::forward<_Args>(__args)...)));
    template 
    void destroy(_Up *__p) noexcept(noexcept(__p->~_Up()));
    //...
  };

allocate

allocate函数,用于分配大小为__n个字节内存,返回值是分配所得内存的地址。

  • 如果待分配内存大小__n大于当前进程最大可用内存,那么就会抛出bad_alloc异常。
  • 再调用operator new来分配内存。operator newmalloc作了一层简单的封装,等效于malloc
  • 将指向operator new的返回值类型转换为此次分配对象_Tp的指针类型。

整个过程如下:

    pointer allocate(size_type __n, const void * = static_cast(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 
    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 
    void destroy(_Up *__p) noexcept(noexcept(__p->~_Up()))
    {
      __p->~_Up();
    }

std::allocator

std::allocator 继承__gnu_cxx::new_allocator

template
using __allocator_base = __gnu_cxx::new_allocator<_Tp>;


  template 
  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 
    struct rebind
    {
      typedef allocator<_Tp1> other;
    };

    allocator() noexcept {}
    allocator(const allocator &__a) noexcept : __allocator_base<_Tp>(__a) {}
    allocator &operator=(const allocator &) = default;

    template 
    allocator(const allocator<_Tp1> &) _GLIBCXX_NOTHROW
    { }

    ~allocator() _GLIBCXX_NOTHROW {}

    //...
    // Inherit everything else.
  };

rebind

__gnu_cxx::new_allocatorstd::allocator中都有一个rebind函数,其主要作用:获得类型_Tp1的内存分配器allocator<_Tp1>

   template 
    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>::other,获得的就是用于分配list_node<_Tp>类型的内存分配器 allocator>

list中的实现如下:

    template
    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 >
    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 
    struct __rebind : __replace_first_arg<_Tp, _Up>
    { };  

    // __rebind 特化版本:当分配器 _Tp 有成员函数 rebind 时调用此特化版本
    template 
    struct __rebind<_Tp,
                    _Up,
                    __void_t::other>>
    {
      using type = typename _Tp::template rebind<_Up>::other;
    };

  protected:
    template  using __pointer     = typename _Tp::pointer;
    template  using __c_pointer   = typename _Tp::const_pointer;
    template  using __v_pointer   = typename _Tp::void_pointer;
    template  using __cv_pointer  = typename _Tp::const_void_pointer;
    template  using __pocca = typename _Tp::propagate_on_container_copy_assignment;
    template  using __pocma = typename _Tp::propagate_on_container_move_assignment;
    template  using __pocs  = typename _Tp::propagate_on_container_swap;
    template  using __equal = typename _Tp::is_always_equal;
  };

__rebind

__allocator_traits_base 中最重要的是成员函数__rebind__rebind的模板参数_Tp是分配器类型,根据_Tp来实现重载:

  • 当传入的内存分配器类型_Tp,实现了rebind成员函数时,比如上面的std::allocator,那么就调用__rebind的特化版本:

      template 
      struct __rebind<_Tp,
                      _Up,
                      __void_t::other>>
      {
        using type = typename _Tp::template rebind<_Up>::other;
      };
    

    std::allocator为例,获取Node类型的内存分配器:

    __allocator_traits_base::__rebind, Node>::type  
    // 等价于 
    std::allocator>
    
  • 当传入的分配器_Tp没有实现rebind成员函数时,就调用普通__rebind版本:

        // _Tp需要是分配器
        template 
        struct __rebind : __replace_first_arg<_Tp, _Up>
        { };  
    

    其中__replace_first_arg实现如下。此时,需要自定义一个内存分配器模板_Template

      template 
      struct __replace_first_arg
      { };
    
      // _Template 是个类模板
      template