我可以假设分配器不直接持有他们的内存池(因此可以被复制)吗?
Posted
技术标签:
【中文标题】我可以假设分配器不直接持有他们的内存池(因此可以被复制)吗?【英文标题】:Can I assume allocators don't hold their memory pool directly (and can therefore be copied)? 【发布时间】:2012-07-27 01:36:27 【问题描述】:我正在编写一个容器并希望允许用户使用自定义分配器,但我不知道我应该通过引用还是通过值来传递分配器。
是否保证(或至少,一个合理的假设)分配器对象将不直接包含其内存池,因此复制分配器并期望内存是可以的分配器池是交叉兼容的?还是我总是需要通过引用传递分配器?
(我发现通过引用传递会对性能造成 2 倍以上的损害,因为编译器开始担心别名,所以我是否可以依赖这个假设。)
【问题讨论】:
您能否添加一些您正在尝试做的细节?您显然必须小心有状态的分配器,但某些标准操作(例如从另一个容器移动构造一个容器)具有标准习惯用法。 @KerrekSB:啊,好的。例如,我正在尝试将resize
或reserve
操作操作实现为“复制此容器和交换”操作。如果分配器没有自己的内存池,则此方法有效。 (如果是这样,我可以交换两次,但创建副本可能仍然很昂贵,如果它包含自己的池,可能会溢出堆栈。)
【参考方案1】:
在 C++11 第 17.6.3.5 节分配器要求 [allocator.requirements] 中指定了符合要求的分配器。其中的要求是:
X an Allocator class for type T
...
a, a1, a2 values of type X&
...
a1 == a2 bool returns true only if storage
allocated from each can be
deallocated via the other.
operator== shall be reflexive,
symmetric, and transitive, and
shall not exit via an exception.
...
X a1(a); Shall not exit via an exception.
post: a1 == a
即当你复制一个分配器时,要求两个副本能够删除彼此的指针。
可以想象,可以将内部缓冲区放入分配器,但副本必须保留其他缓冲区的列表。或者,分配器可能有一个不变量,即解除分配始终是无操作的,因为指针始终来自内部缓冲区(来自您自己的,或来自其他副本)。
但无论采用何种方案,副本都必须是“交叉兼容的”。
更新
这是一个符合 C++11 的分配器,它执行“短字符串优化”。为了使其符合 C++11,我必须将“内部”缓冲区放在分配器外部,以便副本相等:
#include <cstddef>
template <std::size_t N>
class arena
static const std::size_t alignment = 16;
alignas(alignment) char buf_[N];
char* ptr_;
std::size_t
align_up(std::size_t n) return n + (alignment-1) & ~(alignment-1);
public:
arena() : ptr_(buf_)
arena(const arena&) = delete;
arena& operator=(const arena&) = delete;
char* allocate(std::size_t n)
n = align_up(n);
if (buf_ + N - ptr_ >= n)
char* r = ptr_;
ptr_ += n;
return r;
return static_cast<char*>(::operator new(n));
void deallocate(char* p, std::size_t n)
n = align_up(n);
if (buf_ <= p && p < buf_ + N)
if (p + n == ptr_)
ptr_ = p;
else
::operator delete(p);
;
template <class T, std::size_t N>
class stack_allocator
arena<N>& a_;
public:
typedef T value_type;
public:
template <class U> struct rebind typedef stack_allocator<U, N> other;;
explicit stack_allocator(arena<N>& a) : a_(a)
template <class U>
stack_allocator(const stack_allocator<U, N>& a)
: a_(a.a_)
stack_allocator(const stack_allocator&) = default;
stack_allocator& operator=(const stack_allocator&) = delete;
T* allocate(std::size_t n)
return reinterpret_cast<T*>(a_.allocate(n*sizeof(T)));
void deallocate(T* p, std::size_t n)
a_.deallocate(reinterpret_cast<char*>(p), n*sizeof(T));
template <class T1, std::size_t N1, class U, std::size_t M>
friend
bool
operator==(const stack_allocator<T1, N1>& x, const stack_allocator<U, M>& y);
template <class U, std::size_t M> friend class stack_allocator;
;
template <class T, std::size_t N, class U, std::size_t M>
bool
operator==(const stack_allocator<T, N>& x, const stack_allocator<U, M>& y)
return N == M && &x.a_ == &y.a_;
template <class T, std::size_t N, class U, std::size_t M>
bool
operator!=(const stack_allocator<T, N>& x, const stack_allocator<U, M>& y)
return !(x == y);
可以这样使用:
#include <vector>
template <class T, std::size_t N> using A = stack_allocator<T, N>;
template <class T, std::size_t N> using Vector = std::vector<T, stack_allocator<T, N>>;
int main()
const std::size_t N = 1024;
arena<N> a;
Vector<int, N> vA<int, N>(a);
v.reserve(100);
for (int i = 0; i < 100; ++i)
v.push_back(i);
Vector<int, N> v2 = std::move(v);
v = v2;
上述问题的所有分配都来自本地arena
,大小为1 Kb。您应该能够通过值或引用传递此分配器。
【讨论】:
+1 确实,这直接回答了我的问题。 (顺便说一句,您对 SO 的其他答案也非常有帮助。)非常感谢! :) @LucDanton:我确实在align_up
中有一个类型-o。但我在你发表评论前几个小时更正了它。因此,如果您正在查看 align_up 的旧错误版本,或者您正在质疑当前环境,我并不肯定。据我所知,return n + (alignment-1) & ~(alignment-1);
是正确的表达方式。我刚刚对其进行了短暂的重新测试,它的行为符合我的预期。
为什么仍然需要调用v.reserve(100)
?我认为这只是为了避免堆分配。我很惊讶使用堆栈分配的内存调用 reserve() 仍然会带来很大的提升。在我的草稿引擎中,使用堆栈分配器 + Reserve() 与仅使用堆栈分配器或常规向量 + Reserve() 相比,性能差异为 25%。
没有必要打电话给v.reserve(100)
。这只是空间和时间的优化。如果您不调用reserve
,您将更快地耗尽堆栈空间,因为arena
不会尝试回收空间,除非释放是最后分配的东西。随着向量从 2 个元素的空间增长到 4 个(例如),在释放 2 个元素的空间之前分配 4 个元素的空间。所以前者永远丢失了。
@MichaelIV:当我写这篇文章时,我是在 OS X / ios 上这样做的,16 字节是最大对齐,std::max_align_t
尚未标准化。今天我会使用 std::max_align_t
而不是 16。MSVC alignas
解决方法:我不是 VS 专家,但 __declspec(align(16))
看起来很有希望。【参考方案2】:
旧的 C++ 标准对符合标准的分配器提出了要求:这些要求包括,如果您有 Alloc<T> a, b
,则有 a == b
,并且您可以使用 b
解除分配使用 a
分配的内容。分配器基本上是无状态的。
在 C++11 中,情况变得更加复杂,因为现在支持 stateful 分配器。当您复制和移动对象时,如果分配器不同,是否可以从另一个容器复制或移动一个容器,以及如何复制或移动分配器,都有特定的规则。
首先回答您的问题:不,您绝对可以不假设复制您的分配器是有意义的,您的分配器甚至可能不可复制。
这是关于这个主题的 23.2.1/7:
除非另有说明,本节中定义的所有容器都使用分配器获取内存(见 17.6.3.5)。这些容器类型的复制构造函数通过在它们的第一个参数上调用
allocator_traits<allocator_-type>::select_on_container_copy_construction
来获取分配器。移动构造函数通过移动构造从属于被移动容器的分配器中获取分配器。分配器的这种移动构造不应通过异常退出。这些容器类型的所有其他构造函数都采用Allocator&
参数(17.6.3.5),这是一个值类型与容器的值类型相同的分配器。 [注意:如果构造函数的调用使用可选分配器参数的默认值,则分配器类型必须支持值初始化。 —end note] 在每个容器对象的生命周期内或直到分配器被替换之前,此分配器的副本用于由这些构造函数和所有成员函数执行的任何内存分配。分配器只能通过赋值或 交换()。仅当allocator_traits<allocator_type>::propagate_on_container_copy_assignment::value
时,分配器替换通过分配器的复制分配、移动分配或交换执行,allocator_traits<allocator_type>::propagate_on_container_move_assignment::value
或allocator_traits<allocator_type>::propagate_on_container_swap::value
在相应容器操作的实现中为真。除非被交换的对象具有比较相等或allocator_traits<allocator_type>::propagate_on_container_swap::value
为真的分配器,否则调用容器的交换函数的行为是未定义的。在本条款中定义的所有容器类型中,成员get_allocator()
返回用于构造容器的分配器的副本,或者,如果该分配器已被替换,则返回最近替换的副本。
另请参阅std::allocator_traits
的文档以获取概要。
【讨论】:
Oohhhhhhh...所以有一个专门用于此目的的scoped_allocator_adaptor
?我想这意味着我不能盲目地复制它们。 :( +1 很棒的答案,谢谢!
@Mehrdad:作用域适用于您拥有容器容器并且您希望自定义分配器在任何地方一直使用的情况。以上是关于我可以假设分配器不直接持有他们的内存池(因此可以被复制)吗?的主要内容,如果未能解决你的问题,请参考以下文章