基于堆栈缓冲区的STL分配器?

问题描述 投票:37回答:6

[我想知道是否有符合C ++标准库的allocator,它使用驻留在堆栈上的(固定大小的)缓冲区是否可行。

以某种方式,虽然尚未在其他地方隐式回答[[可能,但似乎尚未在SO上这样问过这个问题。

基本上,

似乎

,就我的搜索而言,应该可以创建使用固定大小缓冲区的分配器。现在,乍一看,这应该意味着应该有一个使用固定大小的缓冲区的“分配器”,该缓冲区在栈上“存在”,但是它[[确实出现,没有广泛的这种实现。让我举例说明我的意思:{ ... char buf[512]; typedef ...hmm?... local_allocator; // should use buf typedef std::basic_string<char, std::char_traits<char>, local_allocator> lstring; lstring str; // string object of max. 512 char } 这将如何实现?

answer to this other question(感谢R. Martinho Fernandes)从铬源链接到基于堆栈的分配器:http://src.chromium.org/viewvc/chrome/trunk/src/base/stack_container.h

但是,该类似乎非常特殊,特别是因为此StackAllocator


没有默认的ctor-并且我在想every allocator class needs a default ctor

我想知道是否有一个符合C ++标准库的分配器,该分配器使用驻留在堆栈上的(固定大小的)缓冲区,是否可行。不知何故,似乎这个问题一直没问过...

c++ stl stack allocator
6个回答
19
投票
#include <functional> #include <memory> template <class T, std::size_t N, class Allocator = std::allocator<T>> class stack_allocator { public: typedef typename std::allocator_traits<Allocator>::value_type value_type; typedef typename std::allocator_traits<Allocator>::pointer pointer; typedef typename std::allocator_traits<Allocator>::const_pointer const_pointer; typedef typename Allocator::reference reference; typedef typename Allocator::const_reference const_reference; typedef typename std::allocator_traits<Allocator>::size_type size_type; typedef typename std::allocator_traits<Allocator>::difference_type difference_type; typedef typename std::allocator_traits<Allocator>::const_void_pointer const_void_pointer; typedef Allocator allocator_type; public: explicit stack_allocator(const allocator_type& alloc = allocator_type()) : m_allocator(alloc), m_begin(nullptr), m_end(nullptr), m_stack_pointer(nullptr) { } explicit stack_allocator(pointer buffer, const allocator_type& alloc = allocator_type()) : m_allocator(alloc), m_begin(buffer), m_end(buffer + N), m_stack_pointer(buffer) { } template <class U> stack_allocator(const stack_allocator<U, N, Allocator>& other) : m_allocator(other.m_allocator), m_begin(other.m_begin), m_end(other.m_end), m_stack_pointer(other.m_stack_pointer) { } constexpr static size_type capacity() { return N; } pointer allocate(size_type n, const_void_pointer hint = const_void_pointer()) { if (n <= size_type(std::distance(m_stack_pointer, m_end))) { pointer result = m_stack_pointer; m_stack_pointer += n; return result; } return m_allocator.allocate(n, hint); } void deallocate(pointer p, size_type n) { if (pointer_to_internal_buffer(p)) { m_stack_pointer -= n; } else m_allocator.deallocate(p, n); } size_type max_size() const noexcept { return m_allocator.max_size(); } template <class U, class... Args> void construct(U* p, Args&&... args) { m_allocator.construct(p, std::forward<Args>(args)...); } template <class U> void destroy(U* p) { m_allocator.destroy(p); } pointer address(reference x) const noexcept { if (pointer_to_internal_buffer(std::addressof(x))) { return std::addressof(x); } return m_allocator.address(x); } const_pointer address(const_reference x) const noexcept { if (pointer_to_internal_buffer(std::addressof(x))) { return std::addressof(x); } return m_allocator.address(x); } template <class U> struct rebind { typedef stack_allocator<U, N, allocator_type> other; }; pointer buffer() const noexcept { return m_begin; } private: bool pointer_to_internal_buffer(const_pointer p) const { return (!(std::less<const_pointer>()(p, m_begin)) && (std::less<const_pointer>()(p, m_end))); } allocator_type m_allocator; pointer m_begin; pointer m_end; pointer m_stack_pointer; }; template <class T1, std::size_t N, class Allocator, class T2> bool operator == (const stack_allocator<T1, N, Allocator>& lhs, const stack_allocator<T2, N, Allocator>& rhs) noexcept { return lhs.buffer() == rhs.buffer(); } template <class T1, std::size_t N, class Allocator, class T2> bool operator != (const stack_allocator<T1, N, Allocator>& lhs, const stack_allocator<T2, N, Allocator>& rhs) noexcept { return !(lhs == rhs); }

该分配器使用用户提供的固定大小的缓冲区作为初始内存源,然后在空间不足时退回到辅助分配器(默认为std::allocator<T>)。

需要考虑的事情:

在继续使用堆栈分配器之前,需要考虑您的分配模式。首先,在堆栈上使用内存缓冲区时,您需要考虑

means分配和释放内存的确切条件。

最简单的方法(以及上面采用的方法)是简单地增加堆栈指针以进行分配,并减少堆栈指针以进行释放。请注意,此severely

限制了您实际使用分配器的方式。如果正确使用std::vector(将分配一个连续的内存块),它将很好地工作;但是,std::map则不能以可变的顺序分配和释放节点对象,因此它不能正常工作。

如果您的堆栈分配器只是增加和减少堆栈指针,那么如果您的分配和释放不是按LIFO顺序进行的,则您将获得未定义的行为。如果std::vector会首先从堆栈中分配一个连续的块,然后分配第二个堆栈块,然后再分配第一个块,则也会导致未定义的行为,每次向量将其容量增加到一个仍然为零的值时,都会发生这种情况小于stack_size。这就是为什么您需要提前保留堆栈大小的原因。 (但请参阅以下有关Howard Hinnant实施的说明。)这将我们带到这个问题...

真正想要

来自堆栈分配器的什么?

您实际上是否想要一个通用分配器,该分配器将允许您以不同的顺序分配和释放各种大小的内存块(例如malloc),除了它从预先分配的堆栈缓冲区中提取而不是调用sbrk ?如果是这样,那么您基本上是在谈论实现一种通用分配器,该分配器以某种方式维护一个空闲的内存块列表,只有用户才能为其提供一个预先存在的堆栈缓冲区。这是一个更加复杂的项目。 (如果空间用完了该怎么办?抛出std::bad_alloc?又退回到堆上了吗?)

以上实现假定您需要一个分配器,该分配器将仅使用LIFO分配模式,如果空间不足,则使用另一个分配器。这对于std::vector效果很好,它将始终使用可以连续保留的单个连续缓冲区。当std::vector需要较大的缓冲区时,它将分配较大的缓冲区,复制(或移动)较小缓冲区中的元素,然后取消分配较小的缓冲区。当向量请求更大的缓冲区时,上述stack_allocator实现将简单地退回到辅助分配器(默认为std::allocator。)

因此,例如:

const static std::size_t stack_size = 4; int buffer[stack_size]; typedef stack_allocator<int, stack_size> allocator_type; std::vector<int, allocator_type> vec((allocator_type(buffer))); // double parenthesis here for "most vexing parse" nonsense vec.reserve(stack_size); // attempt to reserve space for 4 elements std::cout << vec.capacity() << std::endl; vec.push_back(10); vec.push_back(20); vec.push_back(30); vec.push_back(40); // Assert that the vector is actually using our stack // assert( std::equal( vec.begin(), vec.end(), buffer, [](const int& v1, const int& v2) { return &v1 == &v2; } ) ); // Output some values in the stack, we see it is the same values we // inserted in our vector. // std::cout << buffer[0] << std::endl; std::cout << buffer[1] << std::endl; std::cout << buffer[2] << std::endl; std::cout << buffer[3] << std::endl; // Attempt to push back some more values. Since our stack allocator only has // room for 4 elements, we cannot satisfy the request for an 8 element buffer. // So, the allocator quietly falls back on using std::allocator. // // Alternatively, you could modify the stack_allocator implementation // to throw std::bad_alloc // vec.push_back(50); vec.push_back(60); vec.push_back(70); vec.push_back(80); // Assert that we are no longer using the stack buffer // assert( !std::equal( vec.begin(), vec.end(), buffer, [](const int& v1, const int& v2) { return &v1 == &v2; } ) ); // Print out all the values in our vector just to make sure // everything is sane. // for (auto v : vec) std::cout << v << ", "; std::cout << std::endl;

参见:http://ideone.com/YhMZxt

同样,这对于矢量来说很好用-但是您需要问自己,您打算对堆栈分配器执行什么操作。如果您想要一个恰好从堆栈缓冲区中提取的通用内存分配器,那么您正在谈论的是一个更加复杂的项目。但是,一个简单的堆栈分配器仅递增和递减堆栈指针将适用于有限的一组用例。请注意,对于非POD类型,您需要使用std::aligned_storage<T, alignof(T)>创建实际的堆栈缓冲区。

[我还要注意,与Howard Hinnant's implementation不同,上述实现未明确检查调用deallocate()时传入的指针是分配的最后一个块。如果传入的指针不是由LIFO排序的释放,则Hinnant的实现将完全不执行任何操作。这将使您无需预先保留即可使用std::vector,因为分配器基本上会

ignore向量尝试释放初始缓冲区的尝试。但是,这也使分配器的语义有些模糊,并且依赖于与std::vector已知工作方式特别相关的行为。我的感觉是,我们也可以简单地说,将通过

last call

返回的deallocate()的任何指针传递给allocate()都会导致未定义的行为,并将其留在那。

*最后-以下警告:debatable看来,检查指针是否在堆栈缓冲区边界内的函数是否甚至是标准定义的行为。比较来自不同new / malloc缓冲区的两个指针的顺序可以说是实现定义的行为(即使使用std::less也是如此),这也许使编写符合标准的堆栈分配器实现无法依靠堆分配成为可能。 (但是实际上,除非您在MS-DOS上运行80286,否则这无关紧要。)

**最后(现在是真的),还值得注意的是,stack allocator中的“ stack”一词已重载,可以同时引用内存的[[source

(固定大小的堆栈数组)和分配的[[method(LIFO递增/递减堆栈指针)。当大多数程序员说他们想要堆栈分配器时,他们在考虑前一种含义而不必考虑后者的语义,以及这些语义如何限制这种分配器在标准容器中的使用。

显然,从一个is a conforming Stack Allocator开始有Howard Hinnant

它通过使用固定大小的缓冲区(通过引用的arena对象,并在请求太多空间时退回到堆中来工作。


8
投票

基于堆栈的STL分配器用途有限,我怀疑您会发现很多现有技术。如果您后来决定要复制或加长首字母lstring,则即使您引用的简单示例也很快就崩溃了。

对于其他STL容器,例如关联的(内部基于树的)STL容器,甚至使用单个或多个连续RAM块的vectordeque,几乎在堆栈上几乎都无法使用内存使用语义任何实际使用情况。


2
投票

2
投票

1
投票
最笨的分配器是单调的冲击分配器,它使用char[]资源作为其基础存储。在原始版本中,char[]通过mmap放置在堆上,但是将其更改为指向堆栈上的char[]并不容易。

0
投票
关于何时可能需要这样的东西:我在嵌入式系统中使用它。嵌入式系统通常对堆碎片反应不佳,因此有时可以使用不会在堆上进行的动态分配。
© www.soinside.com 2019 - 2024. All rights reserved.