Я реализую собственный графический интерфейс/игровой движок и решил внедрить гетерогенную систему управления ресурсами. Как следствие этого, пользовательские распределители очень желательны в ряде ситуаций.
Один из распределителей, который я определил для использования, — это простой линейный распределитель. Насколько я понимаю, базовый линейный распределитель реализован примерно так (для краткости без надлежащей обработки ошибок):
struct linear_allocator
{
using value_type = std::byte;
using pointer = value_type*;
using size_type = std::size_t;
explicit linear_allocator(size_type n) noexcept
{
data = static_cast<pointer>(::operator new(n, std::nothrow_t{}));
}
~linear_allocator() noexcept
{
::operator delete(data, std::nothrow_t{});
}
[[nodiscard]] auto allocate(size_type n) noexcept -> pointer
{
auto temp = position;
position += n;
return temp;
}
auto deallocate(pointer p, size_type n) noexcept -> void
{
position = data;
}
private:
pointer data = nullptr;
pointer position = nullptr;
};
Моя проблема с этой реализацией заключается в ее состоянии. Поскольку это чрезвычайно важный для производительности компонент моего кода, я обеспокоен тем, что это не оптимально и, вполне возможно, подвержено ошибкам.
Поскольку распределитель содержит как указатели на исходный корень данных, так и позицию, он, таким образом, содержит информацию о том, как перебирать память, и о размере данных. Это означает, что мне нужно либо предоставить средства доступа к данным члена, либо предоставить дублирующую информацию о бухгалтерском учете в контейнере, который принимает этот распределитель.
Лучшее решение, на мой взгляд, состоит в том, чтобы контейнер владел информацией о состоянии и оставил выделение контейнеру. Таким образом, контейнер будет содержать всю необходимую информацию, а бухгалтерский учет не будет дублироваться. Кроме того, такое же поведение линейного распределителя может быть достигнуто, если выделение выполняется только в конструкторе контейнеров, а освобождение — только в деструкторе контейнеров. Но если это правда, кажется, что распределителю больше не нужен конструктор или деструктор, и я мог бы также использовать std::allocator. И если это так... какова цель линейного распределителя?
Кажется, я уговорил себя думать, что линейный аллокатор — это антипаттерн. На самом деле я ищу какой-то плотно упакованный гетерогенный контейнер, а линейные распределители кажутся странным слиянием концепций распределителя и контейнера. Пользовательский вектор, в котором хранится бухгалтерская книга (для доступа к индексу, если куча содержит объекты разных размеров) и куча для данных, кажется, больше соответствует тому, что требуется.
Может кто-нибудь объяснить, где должны быть границы между распределителями и контейнерами (особенно в контексте стандартной библиотеки)? Я предполагаю, что в моем обосновании есть ошибка.
Редактировать: основываясь на приведенном ниже предложении Юджина, я переписал свою схему выделения/контейнера следующим образом (обратите внимание, здесь отсутствует конструктор копирования/перемещения, назначение и т. д., которые я только что удалил на время быть... но должно быть реализовано должным образом в какой-то момент). Основной вывод из предоставленного ответа заключается в том, что конструктор и деструкторы command_vector отвечают за распределение, а класс linear_allocator теперь не имеет состояния и прост. Код примерно выглядит следующим образом (я понимаю, что могу и, вероятно, должен вызвать alloc.allocate() один раз, а не выделять здесь 3 отдельных фрагмента):
класс контейнера command_vector
template<typename key_t, typename alloc_t = linear_allocator<std::byte>>
struct command_vector
{
using key_type = key_t;
using value_type = std::byte;
using pointer = value_type*;
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using allocator = alloc_t;
using iterator = command_packet*;
using const_iterator = const iterator;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
explicit command_vector() noexcept :
alloc{},
keys{ alloc.allocate(packet_count) },
packets{ alloc.allocate(packet_count) },
packet_pos{ packets },
heap{ alloc.allocate(heap_size) },
heap_pos{ heap }
{
}
explicit command_vector(size_type max_packets, size_type max_heap) noexcept :
alloc{},
keys{ alloc.allocate(max_packets) },
packets{ alloc.allocate(max_packets) },
packet_pos{ packets },
heap{ alloc.allocate(max_heap) },
heap_pos{ heap }
{
}
constexpr command_vector(key_type* keys, pointer packets, pointer heap) noexcept requires std::is_same_v<alloc_t, null_allocator<value_type>> :
alloc{},
keys{ keys },
packets{ packets },
packet_pos{ packets },
heap{ heap },
heap_pos{ heap }
{
}
constexpr command_vector(const command_vector&) noexcept = delete;
constexpr command_vector(command_vector&&) noexcept = delete;
constexpr auto operator=(const command_vector&) noexcept -> command_vector& = delete;
constexpr auto operator=(command_vector&&) noexcept -> command_vector& = delete;
~command_vector() noexcept
{
if constexpr (!std::is_same_v<allocator, null_allocator<value_type>>)
{
alloc.deallocate(packets);
alloc.deallocate(heap);
}
}
template<typename command_t>
constexpr auto push_back(command_t&& command) noexcept -> void
{
*heap_pos = command;
*packet_pos = make_packet(std::forward<command_t>(command));
heap_pos += sizeof(std::decay_t<command_t>);
++packet_pos;
}
constexpr auto pop_back() noexcept -> void
{
--packet_pos;
heap_pos = static_cast<pointer>(packet_pos->command);
}
constexpr auto clear() noexcept -> void
{
packet_pos = packets;
heap_pos = heap;
}
constexpr auto size() noexcept -> difference_type
{
return packet_pos - packets;
}
constexpr auto begin() -> iterator { return packets; }
constexpr auto end() -> iterator { return packet_pos; }
constexpr auto cbegin() -> const_iterator { return begin(); }
constexpr auto cend() -> const_iterator { return end(); }
constexpr auto rbegin() -> reverse_iterator { return end(); }
constexpr auto rend() -> reverse_iterator { return begin(); }
constexpr auto crbegin() -> const_reverse_iterator { return cend(); }
constexpr auto crend() -> const_reverse_iterator { return cbegin(); }
private:
allocator alloc{};
key_type* keys{};
command_packet* packets{};
command_packet* packet_pos{};
pointer heap{};
pointer heap_pos{};
};
класс null_allocator
template<typename val_t>
struct null_allocator
{
using value_type = val_t;
using pointer = value_type*;
using size_type = std::size_t;
[[nodiscard]] constexpr auto allocate(size_type n) noexcept -> pointer
{
return nullptr;
}
constexpr auto deallocate(pointer p, size_type n) noexcept -> void
{
}
};
класс linear_allocator
template<typename val_t>
struct linear_allocator
{
using value_type = val_t;
using pointer = value_type*;
using size_type = std::size_t;
[[nodiscard]] auto allocate(size_type n) noexcept -> pointer
{
return static_cast<pointer>(::operator new(n, std::nothrow_t{}));
}
auto deallocate(pointer p, size_type n) noexcept -> void
{
::operator delete(p, std::nothrow_t{});
}
};