考虑到复制构造的要求,如何在C ++ 11中编写有状态分配器?

时间:2014-06-18 06:50:17

标签: c++ c++11 stl allocator

据我所知,分配器的要求与STL一起使用 容器在C ++ 11标准的第17.6.3.5节的表28中列出。

我对其中一些要求之间的相互作用感到有些困惑。 给定X类型作为类型T的分配器,类型Y是“ 相应的分配器类“用于类型U,实例aa1a2 该表格X以及b的实例Y表示:

  1. 仅当分配了存储空间时,表达式a1 == a2才会计算为true 来自a1的{​​{1}}可由a2解除分配,反之亦然。

  2. 表达式X a1(a);格式正确,不会通过异常退出, 之后a1 == a是真的。

  3. 表达式X a(b)格式正确,不会通过异常退出,并且 之后a == b

  4. 我读到这句话说所有分配器必须是可复制构造的 副本可与原件互换的方式。更糟糕的是,同样的 跨类型边界是真的。这似乎是一个非常繁重的要求;如 据我所知,这使得大量类型的分配器变得不可能。

    例如,假设我有一个我想在我的分配器中使用的freelist类, 为了缓存释放的对象。除非我遗漏了什么,否则我不能 在分配器中包含该类的实例,因为大小或 TU的对齐方式可能不同,因此空闲列表条目也是如此 不兼容。

    我的问题:

    1. 我的解释是否正确?

    2. 我在一些地方读到C ++ 11改进了对“有状态”的支持 分配器“。鉴于这些限制,情况如何?

    3. 您对如何做我正在尝试的事情有什么建议吗? 做?也就是说,如何在分配器中包含特定于分配类型的状态?

    4. 一般来说,分配器周围的语言似乎很草率。 (例如, 表28的序言说假设a属于X&类型,但有些类型为a 表达式重新定义{{1}}。)此外,至少GCC的支持是不符合要求的。 分配器周围的这种奇怪的原因是什么?它不经常发生 使用过的功能?

3 个答案:

答案 0 :(得分:25)

分配器的平等并不意味着它们必须具有完全相同的内部状态,只是它们必须能够释放分配给任一分配器的内存。 类型a == b的分配器a和类型X的分配器b的分配器Y的交叉类型相等定义表28“与a == Y::template rebind<T>::other(b)相同”。换句话说,a == b如果由a分配的内存可以由重新绑定ba的{​​{1}}实例化的分配器释放。

您的freelist分配器无需释放任意类型的节点,您只需要确保value_type分配的内存可以由FreelistAllocator<T>解除分配。鉴于在大多数理智的实现中FreelistAllocator<U>::template rebind<T>::otherFreelistAllocator<U>::template rebind<T>::other的类型相同,这很容易实现。

简单示例(Live demo at Coliru):

FreelistAllocator<T>

答案 1 :(得分:12)

  

1)我的解释是否正确?

你的自由列表可能不适合分配器,你是对的,它需要能够处理多种尺寸(和对齐)以适应。这是解决自由列表的问题。

  

2)我曾在一些地方读到C ++ 11改进了对有状态分配器的支持&#34;。考虑到这些限制,情况如何?

与出生相比,它没有那么多改进。在C ++ 03中,标准只是推动实现者提供可以支持非平等实例和实现者的分配器,有效地使有状态分配器不可移植。

  

3)你有什么建议可以做我想做的事吗?也就是说,如何在分配器中包含特定于分配类型的状态?

您的分配器可能必须是灵活的,因为您不应该确切地知道应该分配哪些内存(以及哪些类型)。此要求对于将您(用户)与使用分配器的某些容器(如std::liststd::setstd::map)的内部隔离是必要的。

您仍然可以将此类分配器与简单容器一起使用,例如std::vectorstd::deque

是的,这是一项代价高昂的要求。

  

4)一般来说,分配器周围的语言似乎很草率。 (例如,表28的序言说假设a是X&amp;类型,但有些表达式重新定义了a。)此外,至少GCC的支持是不符合的。分配器周围的这种奇怪的原因是什么?它只是一种不经常使用的功能吗?

标准一般不容易阅读,不仅仅是分配器。你必须要小心。

要谨慎,gcc不支持分配器(它是编译器)。我猜你说的是libstdc ++(gcc附带的标准库实现)。 libstdc ++是 old ,因此它是为C ++ 03量身定制的。它已经适应了C ++ 11,但还不完全一致(例如,仍然使用Copy-On-Write作为字符串)。原因是libstdc ++非常注重二进制兼容性,C ++ 11所需的一些更改会破坏这种兼容性;因此,必须仔细介绍它们。

答案 2 :(得分:9)

  

我读到这一点时说,所有分配器必须是可复制构造的,使得副本可以与原件互换。更糟糕的是,跨类型边界也是如此。这似乎是一个非常繁重的要求;据我所知,这使得大量类型的分配器变得不可能。

如果分配器是一些内存资源的轻量级句柄,那么满足要求是微不足道的。只是不要尝试将资源嵌入到单个分配器对象中。

  

例如,假设我有一个我想在分配器中使用的freelist类,以便缓存释放的对象。除非我遗漏了某些内容,否则我无法在分配器中包含该类的实例,因为T和U的大小或对齐可能不同,因此freelist条目不兼容。

[allocator.requirements]第9段:

  

分配器可以约束可以实例化的类型以及可以调用其construct成员的参数。如果某个类型不能与特定分配器一起使用,则分配器类或对construct的调用可能无法实例化。

除了给定类型T之外,您的分配器可以拒绝为任何内容分配内存。这将阻止它在基于节点的容器中使用,例如std::list需要分配它们自己的内部节点类型(不仅仅是容器的value_type),但它适用于std::vector

这可以通过阻止分配器反弹到其他类型来完成:

class T;

template<typename ValueType>
class Alloc {
  static_assert(std::is_same<ValueType, T>::value,
    "this allocator can only be used for type T");
  // ...
};

std::vector<T, Alloc<T>> v; // OK
std::list<T, Alloc<T>> l;   // Fails

或者您只能支持适合sizeof(T)的类型:

template<typename ValueType>
class Alloc {
  static_assert(sizeof(ValueType) <= sizeof(T),
    "this allocator can only be used for types not larger than sizeof(T)");
  static_assert(alignof(ValueType) <= alignof(T),
    "this allocator can only be used for types with alignment not larger than alignof(T)");

  // ...
};
  
      
  1. 我的解释是否正确?
  2.   

不完全。

  
      
  1. 我在一些地方读到C ++ 11改进了对“有状态分配器”的支持。鉴于这些限制,情况如何呢?
  2.   

C ++ 11之前的限制更糟糕!

现在清楚地说明了分配器在复制和移动时如何在容器之间传播,以及当分配器实例被另一个可能与原始实例无法比较的实例替换时,各种容器操作的行为方式。没有这些澄清,不清楚如果发生什么应该发生你用有状态的分配器交换了两个容器。

  
      
  1. 你有什么建议可以做我想做的事吗?也就是说,如何在分配器中包含特定于分配类型的状态?
  2.   

不要将它直接嵌入到分配器中,单独存储它并让分配器通过指针引用它(可能是智能指针,具体取决于您如何设计资源的生命周期管理)。实际的allocator对象应该是一些外部内存源的轻量级句柄(例如竞技场,游戏池或管理空闲列表的东西)。共享相同源的Allocator对象应该相等,即使对于具有不同值类型的分配器也是如此(见下文)。

我还建议您不要尝试支持所有类型的分配,如果您只需要支持它。

  
      
  1. 一般来说,分配器周围的语言似乎很草率。 (例如,表28的序言说假设a是X&amp;类型,但有些表达式重新定义了a。)
  2.   

是的,正如您在https://github.com/cplusplus/draft/pull/334报告的那样(谢谢)。

  

此外,至少GCC的支持是不符合的。

这不是100%,但会在下一个版本中发布。

  

分配器周围的这种奇怪的原因是什么?它只是一种不经常使用的功能吗?

是。并且有许多历史包袱,并且很难指定广泛有用。我的ACCU 2012 presentation有一些细节,如果读完之后我会非常惊讶你认为你可以让它更简单; - )


关于分配器何时比较相等,请考虑:

MemoryArena m;
Alloc<T> t_alloc(&m);
Alloc<T> t_alloc_copy(t_alloc);
assert( t_alloc_copy == t_alloc ); // share same arena
Alloc<U> u_alloc(t_alloc);
assert( t_alloc == u_alloc ); // share same arena
MemoryArena m2
Alloc<T> a2(&m2);
assert( a2 != t_alloc ); // using different arenas

分配器相等的意思是对象可以释放彼此的内存,所以如果你从t_alloc(t_alloc == u_alloc)分配一些内存是true,那么这意味着你可以解除分配内存使用u_alloc。如果它们不相等,则u_alloc无法释放来自t_alloc的内存。

如果您只有一个空闲列表,其中任何内存都可以添加到任何其他空闲列表中,那么您的所有分配器对象可能会相互比较。