gpt4 book ai didi

c++ - 在没有分配器的情况下在容器中遵守传播_on_copy_assignment 的习语

转载 作者:可可西里 更新时间:2023-11-01 15:23:00 24 4
gpt4 key购买 nike

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

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

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

事实上,我们甚至不必写它(因为它只是默认的复制赋值运算符所做的),但让我们说在这种情况下,默认运算符不会执行一些额外的必需逻辑:
Container& operator = (const Container& c) 
{
/* some other stuff... */
m_vec = c.m_vec;
return *this;
}

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

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

假设我们有一个包含指针的内部 vector : 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为真。如果是,我们需要使用另一个容器的分配器来构造复制的节点。

但是...我们的容器类型 Container不使用它自己的分配器。它只使用内部 std::vector ...那么我们如何告诉我们的内部 std::vector必要时使用复制分配器的实例?该 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(); 构造我们的节点

这是创建一个不保留自己的分配器而是遵循内部标准容器的分配器感知容器的有效习惯用法吗?

最佳答案

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

这种方法的第二个问题是,如果 propagate_on_assignmentpropagate_on_container_swap == true_type但是 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;
// ...

IE。 Container模板位于 TAlloc ,并且实现允许 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>”。这被命名为 NodeAlloc在上面的例子中。为这个分配器形成特征也很方便,称为 NodeTraits以上。

第一份工作是给 转换 lhs 分配器的拷贝(从 allocator<node*> 转换为 allocator<node> ),并使用该分配器来销毁和解除分配 lhs 节点。 std::addressof需要将可能的“花式指针”转换为实际 node*在调用 destroy .

接下来,这有点微妙,我们需要传播 m_vec.get_allocator()m_vec ,但前提是 propagate_on_container_copy_assignment是真的。 vector 的复制赋值运算符是最好的方法。这不必要地复制了一些 NodePtr s,但是我仍然相信这是传播该分配器的最佳方式。如果 propagate_on_container_copy_assignment,我们也可以进行 vector 分配为假,从而避免了 if 语句。如果 propagate_on_container_copy_assignment,分配将不会传播分配器是假的,但是我们仍然可以分配一些 NodePtr s,当我们真正需要的是一个空操作。

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

propagate_on_container_copy_assignment是假的,这意味着我们不需要担心分配器不相等的情况。我们不会交换任何资源。

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

作为优化,我们可以 reserveother.size() ,以防 lhs 容量不足。这条线不是正确性所必需的。这是一个纯粹的优化。

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

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

为了异常安全,我们应该使用一个 RAII 设备在我们构造它时保存分配的指针,并将其 push_back 到 vector 中。结构可以抛出, 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_);
}
};

注意 std::allocator_traits 的一致使用用于对分配器的所有访问。这允许 std::allocator_traits提供默认值,以便 Alloc 的作者不需要提供它们。例如 std::allocator_traits可以为 construct 提供默认实现, destroy , 和 propagate_on_container_copy_assignment .

还要注意始终避免 NodePtr 的假设。是 node* .

关于c++ - 在没有分配器的情况下在容器中遵守传播_on_copy_assignment 的习语,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27494945/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com