假设您有一个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();
这是否是创建分配器感知容器的有效习惯用法,该容器不保留自己的分配器,而是推迟到内部标准容器?
答案 0 :(得分:5)
在此示例中使用swap
实现复制分配的一个问题是,如果propagate_on_assignment == true_type
和propagate_on_container_swap == false_type
,则分配器不会从other
传播到{{1因为*this
拒绝这样做。
这种方法的第二个问题是,如果同时swap
和propagate_on_assignment
只有propagate_on_container_swap == true_type
,那么你确实传播了分配器但是在{{1}点处得到了未定义的行为}。
要做到这一点,你真的需要从第一个校长设计你的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;
// ...
上模仿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
可以为Alloc
,std::allocator_traits
和construct
提供默认实现。
另请注意,始终避免destroy
为propagate_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;
}
这种方法做了一些假设,但是:
复制构造函数以[可选]传递分配器的形式实现:
Container::Container(Container const& other, Alloc allcoator = Alloc()));
它假定swap()
适当地交换分配器。
值得指出的是,这种方法具有相当简单的优点,并提供强大的异常保证,但它使用新分配的内存。如果重用LHS对象的存储器,则可以产生更好的性能,例如,因为所使用的存储器已经合理地靠近处理器。也就是说,对于初始实现,我将使用复制和交换实现(使用如上所述的扩展复制构造函数),并在分析表明需要时通过更复杂的实现替换它。