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

Sil*_*ler 11 c++ vector allocator c++11

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

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

Container& operator = (const Container& c) { m_vec = c.m_vec; return *this; }
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

假设我们有一个包含指针的内部向量: 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; 
}
Run Code Online (Sandbox Code Playgroud)

因此,在上面的示例中,我们需要手动释放所有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());
}
Run Code Online (Sandbox Code Playgroud)

...然后我们可以用返回值构造我们的节点 m_vec.get_allocator();

这是一个有效的习惯用法,用于创建一个不支持自己的分配器的分配器感知容器,而是推迟到内部标准容器?

How*_*ant 5

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

这种方法的第二个问题是,如果propagate_on_assignmentand和propagate_on_container_swap == true_typebut other.m_vec.get_allocator() != m_vec.get_allocator()都传播了分配器,但是在的时候得到了未定义的行为swap

为了做到这一点,您确实需要operator=从头开始设计主体。对于本练习,我假设这Container看起来像这样:

template <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;
    // ...
Run Code Online (Sandbox Code Playgroud)

ContainerT和上进行模板化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; 
}
Run Code Online (Sandbox Code Playgroud)

说明:

为了使用兼容的分配器分配和释放内存,我们需要使用std::allocator_traits创建一个“ allocator<node>”。NodeAlloc在上面的示例中将其命名。为上述分配器形成特征也很方便NodeTraits

第一任务是一个变换的LHS分配器(从转换的拷贝allocator<node*>allocator<node>),并使用该分配器来破坏和解除分配LHS节点。 std::addressof需要将可能的“奇特指针”转换为node*对的调用中的实际指针destroy

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

如果propagate_on_container_copy_assignment为true,并且两个分配器不相等,则vector副本分配运算符将在分配分配器之前为我们正确处理lhs资源的转储。这是一个易于忽略的复杂情况,因此最好由vector副本分配运算符决定。

如果propagate_on_container_copy_assignment为false,则意味着我们不必担心分配器不相等的情况。我们不会交换任何资源。

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

作为一种优化,我们可以reserveother.size(),以防LHS能力是不够的。该行对于正确性不是必需的。这是一个纯粹的优化。

以防万一m_vec.get_allocator()现在可能返回一个新的分配器,我们继续获取它的新副本,alloc2上面命名。

现在alloc2,我们可以用来分配,构造和存储从rhs复制构造的新节点。

为了安全起见,在构造指针时应使用RAII设备来保存分配的指针,并将其推入向量中。任何一种构造都可以抛出,而push_back()。RAII设备必须知道在异常情况下是只需要释放还是需要销毁和释放。RAII设备还需要具有“花式指针”意识。事实证明,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_);
    }
};
Run Code Online (Sandbox Code Playgroud)

请注意std::allocator_traits对分配器的所有访问的一致用法。这允许std::allocator_traits提供默认值,以便Alloc不需要的作者提供它们。例如std::allocator_traits可以提供默认的实现constructdestroypropagate_on_container_copy_assignment

还要注意避免一致的假设NodePtrnode*


Die*_*ühl 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;
}
Run Code Online (Sandbox Code Playgroud)

不过,这种方法做出了一些假设:

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

    Container::Container(Container const& other, Alloc allcoator = Alloc()));
    
    Run Code Online (Sandbox Code Playgroud)
  2. 它假设swap()适当地交换分配器。

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