容器使用的内部类型的内存分配

Memory allocation of internal types used by the containers

本文关键字:类型 内存 分配 内部      更新时间:2023-10-16

C++11标准在通用容器要求中有以下几行。

(23.2.1-3)

对于受本子条款影响的声明分配器类型的组件,存储在这些组件中的对象应使用分配器_ raits::construct函数构造,并使用分配器_ traits::destroy函数销毁(20.6.8.2)。这些函数仅针对容器的元素类型调用,不针对容器使用的内部类型调用

(23.2.1-7)

除非另有说明,此子句中定义的所有容器都使用分配器获取内存

容器使用的所有内存都是由指定的分配器分配的,这是真是假?因为标准规定内部类型不是用allocator_traits::construct构造的,所以应该有某种对运算符new的调用。但标准还说,这个子句中定义的所有容器都使用分配器来获得内存,在我看来,这意味着它不能是普通的新操作符,它必须是放置新操作符。我说得对吗?

让我给你举个例子,为什么这很重要。

假设我们有一个类,它包含一些分配的内存:

#include <unordered_map>
#include <iostream>
#include <cstdint>
#include <limits>
#include <memory>
#include <new>
class Arena
{
public:
        Arena(std::size_t size)
        {
                size_     = size;
                location_ = 0;
                data_ = nullptr;
                if(size_ > 0)
                        data_ = new(std::nothrow) uint8_t[size_];
        }
        Arena(const Arena& other) = delete;
        ~Arena()
        {
                if(data_ != nullptr)
                        delete[] data_;
        }
        Arena& operator =(const Arena& arena) = delete;
        uint8_t* allocate(std::size_t size)
        {
                if(data_ == nullptr)
                        throw std::bad_alloc();
                if((location_ + size) >= size_)
                        throw std::bad_alloc();
                uint8_t* result = &data_[location_];
                location_ += size;
                return result;
        }
        void clear()
        {
                location_ = 0;
        }
        std::size_t getNumBytesUsed() const
        {
                return location_;
        }
private:
        uint8_t* data_;
        std::size_t location_, size_;
};

我们还有自定义分配器:

template <class T> class FastAllocator
{
public:
        typedef T value_type;
        typedef T*       pointer;
        typedef const T* const_pointer;
        typedef T&       reference;
        typedef const T& const_reference;
        typedef std::size_t    size_type;
        typedef std::ptrdiff_t difference_type;
        template <class U> class rebind
        {
        public:
                typedef FastAllocator<U> other;
        };
        Arena* arena;
        FastAllocator(Arena& arena_): arena(&arena_) {}
        FastAllocator(const FastAllocator& other): arena(other.arena) {}
        template <class U> FastAllocator(const FastAllocator<U>& other): arena(other.arena) {}
        //------------------------------------------------------------------------------------
        pointer allocate(size_type n, std::allocator<void>::const_pointer)
        {
                return allocate(n);
        }
        pointer allocate(size_type n)
        {
                return reinterpret_cast<pointer>(arena->allocate(n * sizeof(T)));
        }
        //------------------------------------------------------------------------------------
        void deallocate(pointer, size_type) {}
        //------------------------------------------------------------------------------------
        size_type max_size() const
        {
                return std::numeric_limits<size_type>::max();
        }
        //------------------------------------------------------------------------------------
        void construct(pointer p, const_reference val)
        {
                ::new(static_cast<void*>(p)) T(val);
        }
        template <class U> void destroy(U* p)
        {
                p->~U();
        }
};

这就是我们使用它的方式:

typedef std::unordered_map<uint32_t, uint32_t, std::hash<uint32_t>, std::equal_to<uint32_t>,
                           FastAllocator<std::pair<uint32_t, uint32_t>>> FastUnorderedMap;
int main()
{
        // Allocate memory in arena
        Arena arena(1024 * 1024 * 50);
        FastAllocator<uint32_t> allocator(arena);
        FastAllocator<std::pair<uint32_t, uint32_t>> pairAllocator(arena);
        FastAllocator<FastUnorderedMap> unorderedMapAllocator(arena);
        FastUnorderedMap* fastUnorderedMap = nullptr;
        try
        {
                // allocate memory for unordered map
                fastUnorderedMap = unorderedMapAllocator.allocate(1);
                // construct unordered map
                fastUnorderedMap =
                        new(reinterpret_cast<void*>(fastUnorderedMap)) FastUnorderedMap
                        (
                                0,
                                std::hash<uint32_t>(),
                                std::equal_to<uint32_t>(),
                                pairAllocator
                        );
                // insert something
                for(uint32_t i = 0; i < 1000000; ++i)
                        fastUnorderedMap->insert(std::make_pair(i, i));
        }
        catch(std::bad_alloc badAlloc)
        {
                std::cout << "--- BAD ALLOC HAPPENED DURING FAST UNORDERED MAP INSERTION ---" << std::endl;
        }
        // no destructor of unordered map is called!!!!
        return 0;
}

正如您所看到的,undered_map的析构函数从未被调用,但内存在竞技场对象的销毁过程中被释放。会有内存泄漏吗?为什么?

我真的很感激在这个话题上能得到任何帮助。

分配器应该提供4个功能(此处感兴趣):

  • 2用于内存管理:allocate/deallocate
  • 2用于对象生存期管理:construct/destroy

您引用的这些函数仅适用于constructdestroy(在前一句中提到),而不适用于allocate/deallocate,因此不存在矛盾。

现在,关于内存泄漏,为了让arena分配器工作,容器中的对象不仅应该使用arena分配器来构建(容器保证),而且这些对象分配的所有内存也应该从这个分配器中获得;不幸的是,这可能会变得稍微复杂一些。