【问题标题】:Making use of allocators in a custom container class在自定义容器类中使用分配器
【发布时间】:2014-02-08 21:16:36
【问题描述】:

我正在开发一个类似容器的类,我想像使用标准容器一样使用标准分配器基础结构。在网上我找到了很多关于如何单独使用 std::allocator 类,或者如何为标准容器定义自定义分配器的材料,但是关于如何普遍使用符合标准的分配器的材料非常罕见,在特别是在 C++11 的上下文中,从谁编写自定义分配器的角度来看,事情似乎要容易得多,但从容器的角度来看,事情要复杂得多。

所以我的问题是关于如何以最通用的方式正确使用符合标准的分配器,具体来说:

  • 首先,我什么时候应该用这种方式设计自定义容器?使用默认分配器而不是简单的 new/delete 是否存在合理的性能开销(包括缺少优化机会)?
  • 我必须显式调用包含对象的析构函数吗?
  • 如何区分有状态和无状态分配器?
  • 如何处理有状态分配器?
    • 什么时候(如果有的话)两个实例可以互换(我什么时候可以用一个实例销毁分配给另一个实例的内存)?
    • 复制容器的时候也要复制吗?
    • 容器移动时可以/必须移动它们吗?
    • 在容器的移动构造函数和移动赋值运算符中,何时可以将指针移动到分配的内存,何时必须分配不同的内存并移动元素?
  • 在这种情况下是否存在异常安全问题?

我对有关 C++11 世界的答案特别感兴趣(它会改变 C++14 中的任何内容吗?)

【问题讨论】:

  • 分配器通常用作容器的基类。这样,由于空基类优化 (EBCO),无状态分配器不会使您的容器变大。
  • 如果有人想在常规内存中定位一个 std::map 并在共享内存中定位另一个映射,更改分配器支持,而更改 new/delete 则不支持。
  • 您可能希望熟悉 std::allocator_traits 和 C++11 容器 w.r.t 复制/移动/分配/交换的行为,例如vector::operator=
  • 这种行为是在标准中为所有容器都指定的,还是每个都有自己的怪癖?
  • @gigabytes 在 23.2.1 通用容器要求 [container.requirements.general]/13 中为所有适当容器指定:“本条款中定义的所有容器 [条款 23 定义标准容器]并且在 (21.4) [21.4 is basic_string] 中,除了 array 满足分配器感知容器的附加要求,如表 99 中所述。"

标签: c++ memory-management c++11


【解决方案1】:

在下面的所有答案中,我假设您要遵循 C++11 标准定义容器的规则。该标准不要求您以这种方式编写自定义容器。

  • 首先,我什么时候应该用这种方式设计自定义容器?是否存在合理的性能开销(包括缺失 优化机会)在使用默认分配器而不是 简单的新建/删除?

出于性能原因,自定义分配器最常见和最有效的用途之一是让它在堆栈外分配。如果您的自定义容器不能接受这样的分配器,那么您的客户端将无法执行这样的优化。

  • 我必须显式调用包含对象的析构函数吗?

您必须显式调用allocator_traits<allocator_type>::destroy(alloc, ptr),这又会直接调用value_type 的析构函数,或者调用allocator_type 的destroy 成员。

  • 如何区分有状态和无状态分配器?

我不会打扰。假设分配器是有状态的。

  • 如何处理有状态分配器?

非常小心地遵循 C++11 中规定的规则。特别是那些在 [container.requirements.general] 中指定的分配器感知容器。规则太多,这里就不一一列举了。不过,我很乐意回答有关任何这些规则的具体问题。但第一步是获取标准的副本,并阅读它,至少是容器要求部分。为此我推荐the latest C++14 working draft。

  • 什么时候(如果有的话)两个实例可以互换(我什么时候可以用一个实例销毁分配给另一个实例的内存)?

如果两个分配器比较相等,那么任何一个都可以释放从另一个分配的指针。副本(通过复制构造或复制分配)需要比较相等。

  • 复制容器的时候也要复制吗?

在标准中搜索 propagate_on 和 select_on_container_copy_construction 以获取详细信息。简单的回答是“视情况而定”。

  • 容器移动时可以/必须移动它们吗?

必须用于移动构造。移动分配取决于propagate_on_container_move_assignment。

  • 在容器的移动构造函数和移动赋值运算符中,何时可以将指针移动到分配的内存,何时必须分配不同的内存 而是移动元素?

新的 move 构造的容器应该通过 move 构造 rhs 的分配器来获得它的分配器。这两个分配器需要比较相等。因此,您可以转移所有已分配内存的内存所有权,您的容器对于该指针的有效状态为 rhs 中的 nullptr。

移动赋值运算符可以说是最复杂的:行为取决于propagate_on_container_move_assignment 以及两个分配器是否比较相等。更完整的描述在我的“分配器备忘单”中。

  • 在这种情况下是否存在异常安全问题?

是的,吨。 [allocator.requirements] 列出容器可以依赖的分配器要求。这包括哪些操作可以抛出,哪些不能抛出。

您还需要处理分配器的pointer 实际上不是value_type* 的可能性。 [allocator.requirements] 也是寻找这些细节的地方。

祝你好运。这不是一个初学者项目。如果您有更具体的问题,请将它们发布到 SO。要开始,请直接进入标准。我不知道有关该主题的任何其他权威来源。

这是我为自己制作的一份备忘单,其中描述了分配器的行为以及容器的特殊成员。它是用英文写的,不是标准的eze。如果您发现我的备忘单和 C++14 工作草案之间有任何差异,请相信工作草案。一个已知的差异是我添加了noexcept 规范,而标准没有。


分配器行为:

C() noexcept(is_nothrow_default_constructible<allocator_type>::value);

C(const C& c);

从alloc_traits::select_on_container_copy_construction(c)获取分配器。

C(const C& c, const allocator_type& a);

从a获取分配器。

C(C&& c)
  noexcept(is_nothrow_move_constructible<allocator_type>::value && ...);

从move(c.get_allocator())获取分配器,转移资源。

C(C&& c, const allocator_type& a);

从a 获取分配器。如果a == c.get_allocator(),则传输资源。 如果a != c.get_allocator(),则从每个c[i] 移动构造。

C& operator=(const C& c);

如果alloc_traits::propagate_on_container_copy_assignment::value 是true, 复制分配分配器。在这种情况下,如果分配器先验不相等 分配,从*this转储所有资源。

C& operator=(C&& c)
  noexcept(
    allocator_type::propagate_on_container_move_assignment::value &&
    is_nothrow_move_assignable<allocator_type>::value);

如果alloc_traits::propagate_on_container_move_assignment::value 是true, 转储资源,移动分配分配器,并从c 转移资源。

如果alloc_traits::propagate_on_container_move_assignment::value 是false 和get_allocator() == c.get_allocator(),转储资源,转移 来自c的资源。

如果alloc_traits::propagate_on_container_move_assignment::value 是false 和get_allocator() != c.get_allocator(),移动分配每个c[i]。

void swap(C& c)
  noexcept(!allocator_type::propagate_on_container_swap::value ||
           __is_nothrow_swappable<allocator_type>::value);

如果alloc_traits::propagate_on_container_swap::value 是true,则交换 分配器。无论如何,交换资源。未定义的行为,如果 分配器不相等,propagate_on_container_swap::value 是 false。

【讨论】:

  • 谢谢!实际上,“哪个是正确的用法?”太模糊了......在最后一个项目符号中,当我谈到“移动构造函数”时,我应该写“移动构造函数和移动赋值运算符”。当然在移动构造函数中唯一要做的就是移动资源,但是赋值呢?我已经编辑了问题。
  • 问题已更新。最好的信息在我的“备忘单”中,C&amp; operator=(C&amp;&amp; c)。
  • libc++ 向量使用 rhs 迭代器的 move_iterator 适配器调用成员 assign(iter, iter)。成员assign 将在必要时重新分配,在这种情况下将元素从旧缓冲区移动到新缓冲区。然后它将分配给现有元素,并在需要时在末尾构造新元素,或者在需要时从末尾删除元素。 llvm.org/svn/llvm-project/libcxx/trunk/include/vector
  • 据我所知,libc++ 在这件事上是符合要求的。表 99 描述了以 a = rv 开头的行上的分配器感知容器移动赋值运算符的要求。要求还列出了value_type 上的要求。并且向量移动赋值运算符不能对此处未列出的value_type 执行任何操作。其他一切都是 QOI。这个特定的运算符(例如)不允许默认构造value_type。但是我没有限制/定义所有合法的实现(这是一项更大的、非平凡的任务)。
  • @TedPercival:要求位于分配器要求部分的表 28 的“后置条件”列中。查看复制构造和移动构造的行。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-17
  • 1970-01-01
相关资源
最近更新 更多