C++线性分配器和容器边界



我正在实现一个自定义GUI/游戏引擎,并选择实现一个异构资源管理器系统。因此,在许多情况下都非常需要自定义分配器。

我确定要使用的分配器之一是一个简单的线性分配器。我的理解是,基本的线性分配器是这样实现的(为了简洁起见,没有适当的错误处理):

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::分配器。如果是这样的话。。。线性分配器的用途是什么?

我似乎说服自己认为线性分配器是一个反模式。实际上,我正在寻找的是某种紧凑的异构容器,而线性分配器似乎是分配器和容器概念的奇怪合并。存储分类账(如果堆包含不同大小的对象,则用于索引访问)和数据堆的自定义向量似乎更符合所需内容。

有人能解释一下分配器和容器之间的边界应该在哪里吗(尤其是在标准库的上下文中)?我认为我的理由是错误的。

编辑:根据Eugene下面的建议,我重写了我的分配/容器方案,如下所示(注意,这缺少了一个复制/移动构造函数、赋值等,我只是暂时删除了它……但应该在某个时候正确实现)。提供的答案主要是command_vector构造函数和析构函数负责分配,而linear_allocater类现在是无状态的且简单的。代码大致如下(我意识到我可以也应该能够一次调用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{});
}
};
请解释分配器和集装箱应该是

您不应该从分配器开始设计——它们是实现细节。从容器开始。选择具有所需接口、最频繁操作的低算法复杂性、迭代器无效保证、异常安全保证的容器。。。

一旦你有了,测量速度。如果程序太慢,并且您的容器是基于节点的,则可以通过自定义分配器来加速它。当然,要击败通用的C++动态内存机制,您必须了解您的使用模式的一些特殊之处。例如,您可能希望一次释放大块内存。

编辑:容器包含与问题相关的所有状态。分配器通常是无状态的——在C++11之前,有状态分配器甚至不是标准的。有状态分配器用于从不同的内存池中分配内存,并且所有状态都与池相关。

您的linear_allocator出现问题。首先,deallocate()应该是空的——所有的内存都应该在其dtor中释放。其次,它不处理复制-这是真正困难的部分-我不确定什么是正确的语义。尽管我是一个经验丰富的C++程序员,但我不敢自己设计这个分配器。

一个自定义向量,用于存储分类账(如果堆包含不同大小的对象),而数据堆似乎更符合要求。

如果您需要一个异构数据容器,那么这听起来是正确的。例如,您可能需要std::vector<std::unique_ptr<BaseClass>>。或者,您可以考虑std::vector<std::variant<..>>。当然,您可能需要std::vector<>以外的容器。

最新更新