C ++ 11 совместимая реализация линейного распределителя - PullRequest
0 голосов
/ 18 ноября 2018

Я реализовал C ++ 11-совместимый линейный или аренный распределитель.Код следующий:

linear_allocator.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;
}

Не беспокойтесь о alignedMalloc и alignedFree.Они правильные.

Это моя тестовая программа (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 вообще отсутствует.Если я раскомментирую эти 2 строки:

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

вывод вектора также не будет отображаться.

Компиляция с MSVS cl дает следующий вывод:

000000B47A1CAF88
4096

, чтоеще хуже.

Должно быть, я что-то упускаю, так как кажется, что есть UB, но я не могу точно определить, где это.Мой дизайн распределителя был основан на C ++ 11 + рекомендациях.Интересно, что я делаю не так?

1 Ответ

0 голосов
/ 18 ноября 2018

Хотя распределитель заботится о предоставлении и освобождении памяти для хранения данных контейнера, он все равно делает это только по запросу контейнера. То есть фактическое управление предоставленным хранилищем (в частности, его временем жизни) все еще на стороне контейнера. Представьте, что происходит, когда вектор выполняет перемещение своих элементов:

  1. Запрашивается новый фрагмент памяти, который на данный коэффициент больше текущего (старого).

  2. Элементы, хранящиеся в «старом» чанке, копируются / перемещаются в новый чанк.

  3. Только тогда «старый» кусок памяти может быть освобожден.

В вашей реализации за один раз может быть активен только один блок памяти - старый блок памяти освобождается до выделения нового (в частности, это происходит, когда контейнер запрашивает только новый блок памяти, где элементы могут быть перемещены в). Вы вызываете UB уже, когда вектор пытается переместить элементы из предыдущего хранилища, потому что память, в которой они жили, уже признана недействительной.

Кроме того, не предоставляя конструктор копирования для вашего типа распределителя, предоставляемая компилятором реализация выполняет поверхностное копирование (т. Е. Копирует указатель, а не данные, хранящиеся по этому адресу), который затем освобождается в деструкторе. , То есть звонок:

v.get_allocator()

сделает поверхностную копию распределителя, создав значение prvalue вашего типа распределителя, и освободит сохраненный указатель, как только временный объект завершит свое время жизни (т. Е. В конце оператора full, включая вызов cout) , что приводит к двойному вызову alignedFree по тому же указателю.

...