C++11 兼容的线性分配器实现

标签 c++ c++11 memory-management c++17 allocator

我已经实现了一个 C++11 兼容的线性或竞技场分配器。代码如下。

线性分配器.hpp:

#pragma once

#include <cstddef>
#include <cassert>
#include <new>
#include "aligned_mallocations.hpp"

template <typename T>
class LinearAllocator
{
public:
    using value_type = T;
    using pointer = T*;
    using const_pointer = const T*;
    using reference = T&;
    using const_reference = const T&;
    //using propagate_on_container_copy_assignment = std::true_type;
    //using propagate_on_container_move_assignment = std::true_type;
    //using propagate_on_container_swap = std::true_type;

    LinearAllocator(std::size_t count = 64)
        : m_memUsed(0),
        m_memStartAddress(nullptr)
    {
        allocate(count);
    }
    ~LinearAllocator()
    {
        clear();
    }

    template <class U>
    LinearAllocator(const LinearAllocator<U>&) noexcept 
    {}

    /// \brief allocates memory equal to # count objects of type T
    pointer allocate(std::size_t count)
    {
        if (count > std::size_t(-1) / sizeof(T))
        {
            throw std::bad_alloc{};
        }
        if (m_memStartAddress != nullptr)
        {
            alignedFree(m_memStartAddress);
        }
        m_memUsed = count * sizeof(T);
        m_memStartAddress = static_cast<pointer>(alignedMalloc(m_memUsed, alignof(T)));
        return m_memStartAddress;
    }
    /// \brief deallocates previously allocated memory
    /// \brief Linear/arena allocators do not support free() operations. Use clear() instead.
    void deallocate([[maybe_unused]] pointer p, [[maybe_unused]] std::size_t count) noexcept
    {
        //assert(false);
        clear();
    }

    /// \brief simply resets memory
    void clear()
    {
        if (m_memStartAddress != nullptr)
        {
            alignedFree(m_memStartAddress);
            m_memStartAddress = nullptr;
        }
        this->m_memUsed = 0;
    }

    /// \brief GETTERS
    pointer getStartAddress() const
    {
        return this->m_memStartAddress;
    }
    std::size_t getUsedMemory() const
    {
        return this->m_memUsed;
    }
private:
    std::size_t m_memUsed;
    pointer m_memStartAddress;
};

template <class T, class U>
bool operator==(const LinearAllocator<T> &, const LinearAllocator<U> &)
{
    return true;
}

template <class T, class U>
bool operator!=(const LinearAllocator<T> &, const LinearAllocator<U> &)
{
    return false;
}

不用担心 alignedMallocalignedFree。他们是正确的。

这是我的测试程序(linear_allocator.cpp):

#include "linear_allocator.hpp"
#include <vector>
#include <deque>
#include <iostream>
#include <string>
#include <typeinfo>

int main()
{
    [[maybe_unused]]
    LinearAllocator<int> a{1024};
    std::cout << a.getStartAddress() << '\n';
    std::cout << a.getUsedMemory() << '\n';
    std::vector<std::string, LinearAllocator<std::string>> v;
    v.reserve(100);
    std::cout << "Vector capacity = " << v.capacity() << '\n';
    //std::cout << v.get_allocator().getStartAddress() << '\n';
    //std::cout << v.get_allocator().getUsedMemory() << '\n';
    v.push_back("Hello");
    v.push_back("w/e");
    v.push_back("whatever");
    v.push_back("there is ist sofi j");
    v.push_back("wisdom");
    v.push_back("fear");
    v.push_back("there's more than meets the eye");
    for (const auto &s : v)
    {
        std::cout << s << '\n';
    }
    std::cout << typeid(v.get_allocator()).name() << '\n';

    std::deque<int, LinearAllocator<int>> dq;
    dq.push_back(23);
    dq.push_back(90);
    dq.push_back(38794);
    dq.push_back(7);
    dq.push_back(0);
    dq.push_back(2);
    dq.push_back(13);
    dq.push_back(24323);
    dq.push_back(0);
    dq.push_back(1234);
    for (const auto &i : dq)
    {
        std::cout << i << '\n';
    }
    std::cout << typeid(dq.get_allocator()).name() << '\n';
}

使用 g++ -std=c++17 -O2 -march=native -Wall linear_allocator.cpp -o linear_allocator.gpp.exe 编译并运行 linear_allocator.gpp.exe 得到输出:

0x4328b8
4096
Vector capacity = 100
Hello
w/e
whatever
there is ist sofi j
wisdom
fear
there's more than meets the eye
15LinearAllocatorINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

如您所见,deque 的输出根本不存在。如果我取消注释这两行:

//std::cout << v.get_allocator().getStartAddress() << '\n';
//std::cout << v.get_allocator().getUsedMemory() << '\n';

vector 的输出也不会显示。

用 MSVS cl 编译给出以下输出:

000000B47A1CAF88
4096

这更糟。

肯定有什么我遗漏了,因为似乎有 UB,但我无法确定它在哪里。我的分配器设计基于 C++11+ 准则。我想知道我做错了什么。

最佳答案

虽然分配器负责提供和释放用于存储容器数据的内存,但它仍然只在容器请求时才这样做。也就是说,所提供存储的实际管理(特别是其生命周期)仍在容器方面。想象一下当 vector 对其元素执行重定位时会发生什么:

  1. 请求一个新的内存块,比当前(旧)内存块大一个给定因子。

  2. 存储在“旧” block 中的元素被复制/移动到新 block 中。

  3. 只有这样,“旧”内存块才能被释放。

在您的实现中,一次只能有一个内存块处于事件状态——旧的内存块在分配新的内存块之前被释放(具体来说,当容器只请求一个新的内存块时,就会发生这种情况,其中元素可以重新定位到)。当 vector 尝试从先前的存储中重新定位元素时,您已经调用了 UB,因为它们所在的内存已经失效。

此外,通过不为您的分配器类型提供复制构造函数,编译器提供的实现执行浅拷贝(即,它复制指针,而不是存储在该地址下的数据),然后在析构函数中释放.即调用:

v.get_allocator()

将制作分配器的浅表拷贝,创建分配器类型的纯右值,并在临时对象结束其生命周期后立即释放存储的指针(即,在包含 cout< 的完整语句结束时 调用),导致在同一指针上对 alignedFree 进行两次调用。

关于C++11 兼容的线性分配器实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53362386/

相关文章:

c++ - 如何模拟方法返回 istream&?

c++ - 来自二维数组的特征图

c++ - 在标题中定义 cv::Mat 后跟另一个 Mat 避免了多个 channel

c++ - BigInt 实现 - 将字符串转换为存储为 unsigned int 的二进制表示

c++ - 同名的嵌套类和成员函数

c++ - 将对 boost 数组的引用传递给类

c++ - 结构的 cudaMalloc 和相同结构的元素

c++ - 如何在 Windows 不分配盘符的情况下创建分区?

c++ - 如何在 C++11 中捕获函数参数并存储函数指针以供以后执行?

C++ - 从客户端代码中隐藏模板参数