gpt4 book ai didi

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

转载 作者:IT老高 更新时间:2023-10-28 12:34:49 26 4
gpt4 key购买 nike

我想知道有一个符合 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)从 Chrome 源链接到基于堆栈的分配器: http://src.chromium.org/viewvc/chrome/trunk/src/base/stack_container.h

然而,这个类似乎非常奇特,尤其是因为这个 StackAllocator没有默认的构造函数——我在想 every allocator class needs a default ctor .

最佳答案

绝对有可能创建一个完全符合 C++11/C++14 的堆栈分配器*。但是您需要考虑有关堆栈分配的实现和语义以及它们如何与标准容器交互的一些后果。

这是一个完全符合 C++11/C++14 的堆栈分配器(也托管在我的 github 上):

#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>)。

需要考虑的事项:

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

最简单的方法(以及上面使用的方法)是简单地递增分配的堆栈指针,并递减它以进行释放。请注意,这严重限制了您在实践中使用分配器的方式。例如,它可以很好地用于 std::vector (这将分配一个连续的内存块)如果使用得当,但不能用于 std::map ,它将以不同的顺序分配和释放节点对象。

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

这让我们想到了一个问题......

你真正想从堆栈分配器中得到什么?

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

上面的实现假设你想要一个分配器,它会简单地使用 LIFO 分配模式,并在空间不足时回退到另一个分配器。这适用于 std::vector ,它将始终使用可以提前保留的单个连续缓冲区。当 std::vector需要更大的缓冲区,它将分配更大的缓冲区,复制(或移动)较小缓冲区中的元素,然后释放较小的缓冲区。当 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

同样,这对 vector 工作正常 - 但你需要问自己你到底打算用堆栈分配器做什么。如果您想要一个恰好从堆栈缓冲区中提取的通用内存分配器,那么您正在谈论一个更复杂的项目。然而,一个简单的堆栈分配器,它只是增加和减少堆栈指针,将适用于有限的用例集。请注意,对于非 POD 类型,您需要使用 std::aligned_storage<T, alignof(T)>创建实际的堆栈缓冲区。

我还注意到,不像 Howard Hinnant's implementation ,上面的实现没有明确地检查当你调用 deallocate() 时,传入的指针是最后分配的块。如果传入的指针不是 LIFO 顺序的释放,Hinnant 的实现将什么也不做。这将使您能够使用 std::vector无需提前保留,因为分配器基本上会忽略 vector 尝试释放初始缓冲区的尝试。但这也有点模糊了分配器的语义,并且依赖于非常明确地绑定(bind)到方式 std::vector 的行为。已知有效。我的感觉是,我们不妨简单地说,将任何指针传递给 deallocate()这不是通过 返回的最后一次通话 allocate()将导致未定义的行为并将其留在那里。

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

** 最后(现在真的),还值得注意的是,堆栈分配器中的“堆栈”一词有点重载,既指内存源(固定大小的堆栈数组)又指分配方法(LIFO 增量)/递减堆栈指针)。当大多数程序员说他们想要一个堆栈分配器时,他们考虑的是前者的含义,而不必考虑后者的语义,以及这些语义如何限制这种分配器与标准容器的使用。

关于c++ - 基于堆栈缓冲区的 STL 分配器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/8049657/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com