C++ STL中的allocator

绪论

As we all kown,C++中的STL容器使用其内置的std::allocator分配内存。如果我们想要更改STL分配内存的行为,我们不用更改容器的逻辑,只需要更改传入的allocator即可。一直以来,allocator对我来讲都笼罩着一层迷雾,总感觉是一个很复杂很恐怖的东西,最近因为课程原因,需要使用平台特定的内存管理函数(在NVM仿真平台quartz中需要使用其提供的pmalloc和pfree函数管理非易失性内存),就研究了一下std::allocator是怎么实现的。但是认真了解以后发现没有什么神奇的东西。

源码剖析

allocator主要有四个接口:

  • T *allocate(size_t):用void * ::operator new(size_t)分配内存(但是不构造,其实就是对malloc的封装)
  • void deallocate(T*, size_t):使用void ::operator delete(void *)释放内存(其实是对free的封装)
  • void construct(T*, Args&&):使用定位new(placement new::new(void*) T(std::forward<Args>(args)...))在指定内存上进行构造
  • void destroy(T*):调用析构函数(p->~T())进行析构

不过后两者在C++17中被废弃了,在C++20中被删除了,我在stackoverflow上看到一个回答的大概意思是以后应该用std::allocator_traits::construct了。原回答:Why are are std::allocator’s construct and destroy functions deprecated in c++17?

去看了一下STL 的源码,发现std::allocator继承了__allocator_base,然后后者又继承了__gnu_cxx::new_allocator,也就是说其实用的是GNU版本的allocator,我认真阅读了一下__gnu_cxx::new_allocator的实现,发现的确挺简洁,没有什么特殊的东西,通过把里面的宏改成普通的关键字,删除掉我觉得没有什么用的版本判断,就可以得到一个简单的allocator实现:

// Copyright(C), Edward-Elric233
// Author: Edward-Elric233
// Version: 1.0
// Date: 2022/10/25
// Description: __gnu__cxx::new_allocator
#ifndef TEMP_PALLOCATOR_H
#define TEMP_PALLOCATOR_H

#include "utils.h"
#include <new>
#include <type_traits>


namespace edward
{

    using std::size_t;
    using std::ptrdiff_t;


    /**
     *  @brief  An allocator that uses global new, as per [20.4].
     *  @ingroup allocators
     *
     *  This is precisely the allocator defined in the C++ Standard.
     *    - all allocation calls operator new
     *    - all deallocation calls operator delete
     *
     *  @tparam  T  Type of allocated object.
     */
    template<typename T>
    class pallocator
    {
    public:
        typedef size_t     size_type;
        typedef ptrdiff_t  difference_type;
        typedef T*       pointer;
        typedef const T* const_pointer;
        typedef T&       reference;
        typedef const T& const_reference;
        typedef T        value_type;

		/*
		* 在阅读《Modern Effective C++》后发现原文件中的rebind只不过是一个类型别名,可以用模板using代替,
		* 但是在尝试修改sgi-stl的源码的过程中我发现他们的实现依赖于这个rebind,所以还是就用rebind吧
        template<typename T1>
        using other = pallocator<T1>;
        */
        template<typename Tp1>
        struct rebind
        { typedef pallocator<Tp1> other; };




        // _GLIBCXX_RESOLVE_LIB_DEFECTS
        // 2103. propagate_on_container_move_assignment
        typedef std::true_type propagate_on_container_move_assignment;

        constexpr pallocator() noexcept { }

        constexpr
        pallocator(const pallocator&) noexcept { }

        template<typename T1>
        constexpr
        pallocator(const pallocator<T1>&) noexcept { }

        ~pallocator() noexcept { }

        pointer
        address(reference __x) const noexcept
        { return std::__addressof(__x); }

        const_pointer
        address(const_reference __x) const noexcept
        { return std::__addressof(__x); }

        // NB: __n is permitted to be 0.  The C++ standard says nothing
        // about what the return value is when __n == 0.
        [[nodiscard]] pointer   //cannot discard return value!!!
        allocate(size_type __n, const void* = static_cast<const void*>(0))
        {
            if (__n > this->max_size())
                throw std::bad_alloc();
            print("allocate", __n * sizeof(T), "bytes");
            return static_cast<T*>(::operator new(__n * sizeof(T)));
        }

        // __p is not permitted to be a null pointer.
        void
        deallocate(pointer __p, size_type __n)
        {
            print("deallocate", __n * sizeof(T), "bytes");
            ::operator delete(__p);
        }

        size_type
        max_size() const noexcept
        {
            return size_t(__PTRDIFF_MAX__) / sizeof(T);
        }

        template<typename _Up, typename... _Args>
        void
        construct(_Up* __p, _Args&&... __args)
        noexcept(std::is_nothrow_constructible<_Up, _Args...>::value)
        { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }

        template<typename _Up>
        void
        destroy(_Up* __p)
        noexcept(std::is_nothrow_destructible<_Up>::value)
        { __p->~_Up(); }
        template<typename _Up>
        friend bool
        operator==(const pallocator&, const pallocator<_Up>&) noexcept
        { return true; }

        template<typename _Up>
        friend bool
        operator!=(const pallocator&, const pallocator<_Up>&) noexcept
        { return false; }
    };

} // namespace



#endif //TEMP_PALLOCATOR_H

我在allocatedeallocate函数中添加了打印函数,utils.h头文件的获取可以看一下我之前的博客:C++ 工具函数库

后来我尝试修改sgi-stl的源码,想要用自己修改的这个allocator,但是发现有一些问题:

  1. sgi-stl依赖于typename rebind::other重命名
  2. sgi-stl中的allocator的方法都是静态的!!!我调试了好久才发现这个问题,虽然sgi-stl已经算是比较容易阅读的版本了,但是还是有很多的typedef,然后里面还有自己的_Alloc_traits,把我看吐了。

参考资料