用于在没有分配器的Container中服从propagate_on_copy_assignment的习惯用法

时间:2014-12-15 23:18:38

标签: c++ c++11 vector allocator

假设您有一个Container,它在内部使用其他标准容器来形成更复杂的数据结构。值得庆幸的是,标准容器已经设计用于执行所有必要的工作以确保复制/分配分配器等。

所以,通常如果我们有一些容器c,并且在内部它有一个std::vector<int>,我们可以编写一个复制赋值运算符,它只是说:

Container& operator = (const Container& c) { m_vec = c.m_vec; return *this; }

实际上我们甚至不必编写它(因为它只是默认的复制赋值运算符所做的),但我们只是说在这种情况下,默认运算符不会执行一些额外的必需逻辑: / p>

Container& operator = (const Container& c) 
{
   /* some other stuff... */
   m_vec = c.m_vec;
   return *this; 
}

因此,在这种情况下没有问题,因为向量赋值运算符为我们完成了所有工作,以确保分配器正确地复制或不复制。

但是......如果我们有一个我们不能简单地复制分配的矢量怎么办?假设它是指向其他内部结构的指针向量。

假设我们有一个包含指针的内部向量:std::vector<node*, Alloc>

因此,通常在我们的复制赋值运算符中,我们必须说:

Container& operator = (const Container& other) 
{
   vector<node*, Alloc>::allocator_type alloc = m_vec.get_allocator();
   for (auto it = m_vec.begin(); it != m_vec.end(); ++it) alloc.deallocate(*it);
   m_vec.clear();

   for (auto it = other.m_vec.begin(); it != other.m_vec.end(); ++it)
   {
     node* n = alloc.allocate(1); // this is wrong, we might need to use other.get_allocator() here!
     alloc.construct(n, *(*it));
     m_vec.push_back(n);
   }

   return *this; 
}

因此,在上面的示例中,我们需要手动释放node中的所有m_vec对象,然后从RHS容器构造新的节点对象。 (注意,我正在使用vector在内部使用的分配器对象,以便分配节点对象。)

但是如果我们想在这里和AllocatorAware符合标准,我们需要检查allocator_traits<std::vector<node*, Alloc>::allocator_type>是否将propagate_on_container_copy_assign设置为true。如果是这样,我们需要使用另一个容器的分配器来构造复制的节点。

但是...我们的容器类型Container不使用它自己的分配器。它只使用内部std::vector ...那么如何在必要时告诉我们的内部std::vector实例使用复制的分配器?该向量没有像“use_allocator”或“set_allocator”成员函数那样的东西。

所以,我想出的唯一一件事就是:

if (std::allocator_traits<Alloc>::propagate_on_container_copy_assignment::value)
{
   m_vec = std::vector<node*, Alloc>(other.get_allocator());
}

...然后我们可以使用返回值m_vec.get_allocator();

构造我们的节点

这是否是创建分配器感知容器的有效习惯用法,该容器不保留自己的分配器,而是推迟到内部标准容器?

2 个答案:

答案 0 :(得分:5)

在此示例中使用swap实现复制分配的一个问题是,如果propagate_on_assignment == true_typepropagate_on_container_swap == false_type,则分配器不会从other传播到{{1因为*this拒绝这样做。

这种方法的第二个问题是,如果同时swappropagate_on_assignment只有propagate_on_container_swap == true_type,那么你确实传播了分配器但是在{{1}点处得到了未定义的行为}。

要做到这一点,你真的需要从第一个校长设计你的other.m_vec.get_allocator() != m_vec.get_allocator()。对于本练习,我假设swap看起来像这样:

operator=

即。在Containertemplate <class T, class Alloc> struct Container { using value_type = T; static_assert(std::is_same<typename Alloc::value_type, value_type>{}, ""); using allocator_type = Alloc; struct node {}; using NodePtr = typename std::pointer_traits< typename std::allocator_traits<allocator_type>::pointer>::template rebind<node>; using NodePtrAlloc = typename std::allocator_traits<allocator_type>::template rebind_alloc<NodePtr>; std::vector<NodePtr, NodePtrAlloc> m_vec; // ... 上模仿Container,并且该实现允许T使用“花式指针”的可能性(即Alloc实际上是类型)。

在这种情况下,Alloc复制赋值运算符可能如下所示:

node*

说明:

为了使用兼容的分配器分配和释放内存,我们需要使用Container来创建“Container& operator = (const Container& other) { if (this != &other) { using NodeAlloc = typename std::allocator_traits<NodePtrAlloc>::template rebind_alloc<node>; using NodeTraits = std::allocator_traits<NodeAlloc>; NodeAlloc alloc = m_vec.get_allocator(); for (auto node_ptr : m_vec) { NodeTraits::destroy(alloc, std::addressof(*node_ptr)); NodeTraits::deallocate(alloc, node_ptr, 1); } if (typename NodeTraits::propagate_on_container_copy_assignment{}) m_vec = other.m_vec; m_vec.clear(); m_vec.reserve(other.m_vec.size()); NodeAlloc alloc2 = m_vec.get_allocator(); for (auto node_ptr : other.m_vec) { using deleter = allocator_deleter<NodeAlloc>; deleter hold{alloc2, 1}; std::unique_ptr<node, deleter&> n{NodeTraits::allocate(alloc2, 1), hold}; NodeTraits::construct(alloc2, std::addressof(*n), *node_ptr); hold.constructed = true; m_vec.push_back(n.get()); n.release(); } } return *this; } ”。在上面的示例中,这名为std::allocator_traits。为这个分配器形成特征也很方便,上面称为allocator<node>

第一项工作是lhs分配器的转换副本(从NodeAlloc转换为NodeTraits),并将该分配器用于两者销毁和释放lhs节点。在调用allocator<node*>时,需要allocator<node>将可能的“花式指针”转换为实际的std::addressof

接下来,这有点微妙,我们需要将node*传播到destroy,但前提是m_vec.get_allocator()为真。 m_vec的副本赋值运算符是执行此操作的最佳方法。这会不必要地复制一些propagate_on_container_copy_assignment,但我仍然认为这是传播分配器的最佳方法。如果vector为false,我们也可以执行向量赋值,从而避免使用if语句。如果NodePtr为false,则赋值不会传播分配器,但是当我们真正需要的是无操作时,我们仍然可以分配一些propagate_on_container_copy_assignment

如果propagate_on_container_copy_assignment为真且两个分配器不相等,NodePtr复制赋值运算符将在分配分配器之前正确处理为我们转储lhs资源。这是一个容易被忽视的复杂因素,因此最好留给propagate_on_container_copy_assignment复制赋值运算符。

如果vector为false,则表示我们不需要担心我们有不相等的分配器的情况。我们不会交换任何资源。

无论如何,在这之后,我们应该vector lhs。此操作不会转储propagate_on_container_copy_assignment,因此不会浪费。在这一点上,我们有一个零大小的lhs和正确的分配器,甚至可能还有一些非零的clear()

作为优化,我们capacity()可以使用capacity(),以防lhs容量不足。这条线对于正确性不是必需的。这是一个纯粹的优化。

以防reserve现在可能会返回一个新的分配器,我们继续获取它的新副本,名为other.size()

我们现在可以使用m_vec.get_allocator()来分配,构造和存储从rhs构造的新节点。

为了安全异常,我们应该使用RAII设备在构造它时保持分配的指针,并将其推送到向量中。构造可以抛出,alloc2也可以抛出。 RAII设备必须知道它是否需要在异常情况下解除分配,或者是否需要解析和解除分配。 RAII设备也需要“花式指针” - 意识到。事实证明,使用alloc2结合自定义删除器构建所有这些内容非常容易:

push_back()

注意std::unique_ptr对分配器的所有访问的一致使用。这允许template <class Alloc> class allocator_deleter { using traits = std::allocator_traits<Alloc>; public: using pointer = typename traits::pointer; using size_type = typename traits::size_type; private: Alloc& alloc_; size_type s_; public: bool constructed = false; allocator_deleter(Alloc& a, size_type s) noexcept : alloc_(a) , s_(s) {} void operator()(pointer p) noexcept { if (constructed) traits::destroy(alloc_, std::addressof(*p)); traits::deallocate(alloc_, p, s_); } }; 提供默认值,以便std::allocator_traits的作者无需提供默认值。例如,std::allocator_traits可以为Allocstd::allocator_traitsconstruct提供默认实现。

另请注意,始终避免destroypropagate_on_container_copy_assignment的假设。

答案 1 :(得分:1)

利用现有功能似乎是合理的。就个人而言,我会更进一步,实际利用现有的实现来进行复制。通常,似乎合适的复制和交换习惯用法是实现复制分配的最简单方法:

Container& Container::operator= (Container const& other) {
    Container(other,
              std::allocator_traits<Alloc>::propagate_on_assignment
              ? other.get_allocator()
              : this->get_allocator()).swap(*this);
    return *this;
}

这种方法做了一些假设,但是:

  1. 复制构造函数以[可选]传递分配器的形式实现:

    Container::Container(Container const& other, Alloc allcoator = Alloc()));
    
  2. 它假定swap()适当地交换分配器。

  3. 值得指出的是,这种方法具有相当简单的优点,并提供强大的异常保证,但它使用新分配的内存。如果重用LHS对象的存储器,则可以产生更好的性能,例如,因为所使用的存储器已经合理地靠近处理器。也就是说,对于初始实现,我将使用复制和交换实现(使用如上所述的扩展复制构造函数),并在分析表明需要时通过更复杂的实现替换它。