【问题标题】:Is there something similar to the copy-and-swap idiom when allocators are involved?当涉及分配器时,是否有类似于复制和交换习语的东西?
【发布时间】:2013-12-26 01:32:11
【问题描述】:

关于 copy-and-swap 习语有几个很好的答案,例如 explaining the copy and swap idiomexplaining move semantics。适用于复制和移动分配的基本习惯用法如下所示:

T& T::operator=(T other) {
    this->swap(other);
    return *this;
}

此赋值适用于复制和移动赋值,因为 other 是复制还是移动构造取决于赋值的右侧是左值还是右值。

现在让有状态的分配器进入画面:如果T 被参数化为分配器类型,例如std::vector<S, A>,上述习语并不总是有效!具体来说,std::allocator_traits<A> 包含三种类型,指示是否应该传播分配器:

  1. std::allocator_traits<A>::propagate_on_container_copy_assignment
  2. std::allocator_traits<A>::propagate_on_container_move_assignment
  3. std::allocator_traits<A>::propagate_on_container_swap

这三个特征的默认值是std::false_type(参见 20.6.8.1 [allocator.traits.types] 第 7、8 和 9 段)。如果这些特征中的任何一个是std::false_type 并且分配器是有状态的并且可能比较不相等,则正常的复制和交换习语不起作用。对于复制分配,修复相当简单:

T& T::operator= (T const& other) {
    T(other, this->get_allocator()).same_allocator_swap(*this);
    return *this;
}

也就是说,首先复制对象并提供 LHS 的分配器对象,然后使用一个函数交换成员,该函数在两个对象使用相同的分配器时起作用,即other.get_allocator() == this->get_allocator()

移动分配时,如果可以移动,最好不要复制 RHS。如果分配器相同,则可以移动 RHS。否则需要使用适当的分配器复制对象,从而导致像这样的赋值运算符

T& T::operator= (T&& other) {
    T(std::move(other), this->get_allocator()).same_allocator_swap(*this);
    return *this;
}

这里的方法是移动构造一个临时的同时也传递一个分配器。这样做假定T 类型确实有一个“移动构造函数”,它采用T&& 作为对象状态和一个分配器来指定要使用的分配器。根据分配器的不同或相同,将负担放在移动构造函数上进行复制或移动。

由于第一个参数的传递方式不同,因此不能将复制和移动赋值合并到赋值运算符的一个版本中。因此,他们需要将参数作为引用,并且需要显式复制或移动参数以抑制复制省略的可能性。

当涉及分配器时,是否有更好的方法来处理赋值运算符?

【问题讨论】:

    标签: c++ c++11 allocator


    【解决方案1】:

    您似乎暗示经典的复制/交换习语确实适用于所有propagate_on_ 特征都不为假的情况。我不相信这是这样的。例如考虑:

    std::allocator_traits<A>::propagate_on_container_copy_assignment::value == true
    std::allocator_traits<A>::propagate_on_container_swap::value == false
    

    经典的复制/交换习语赋值运算符不会将分配器从 rhs 传播到 lhs,如果两个分配器状态不相等,则会进入未定义行为状态。

    您对复制分配运算符的重写也不适用于这种propagate_on_ 特征的组合,因为它永远不会在复制分配上传播分配器。

    如果一个人想要遵循 std::containers 的规则,我不认为建议使用复制/交换习语。

    我为自己保留了一份“分配器行为”备忘单,其中描述了这些成员的行为方式(用英语而不是标准 eze)。

    复制赋值运算符

    如果propagate_on_container_copy_assignment::value 为真,则复制分配分配器。在这种情况下,如果分配器在分配之前不相等,则首先释放 lhs 上的所有内存。然后继续复制分配值,而不转移任何内存所有权。 IE。 this-&gt;assign(rhs.begin(), rhs.end()).

    移动赋值运算符

    1. 如果propagate_on_container_move_assignment::value 为真,则释放 lhs 上的所有内存,移动分配分配器,然后将内存所有权从 rhs 转移到 lhs。

    2. 如果propagate_on_container_move_assignment::value 为假,并且两个分配器相等,则释放 lhs 上的所有内存,然后将内存所有权从 rhs 转移到 lhs。

    3. 如果propagate_on_container_move_assignment::value 为false,并且两个分配器不相等,则移动assign 如同this-&gt;assign(make_move_iterator(rhs.begin()), make_move_iterator(rhs.end())

    这些描述旨在实现尽可能高的性能,同时遵守容器和分配器的 C++11 规则。只要有可能,内存资源(例如vector capacity())要么从rhs 转移,要么在lhs 上重用。

    复制/交换习语总是会丢弃 lhs 上的内存资源(例如 vector capacity()),而是在将这些资源释放到 lhs 之前临时抢先分配这些资源。 p>

    为了完整性:

    交换

    如果propagate_on_container_swap::value 为真,则交换分配器。无论如何,交换内存所有权。如果 propagate_on_container_swap::value 为 false 并且分配器不相等,则行为未定义。

    【讨论】:

    • 好吧,我的尝试显然有些不完整。您的描述不会产生强大的异常安全分配,即使对于复制分配(至少在某些情况下)也是如此。答案还意味着,没有已知的合理简单的配方可以利用其他方法中实现的功能。
    • 同意基本的异常安全。这是为 std::containers 指定的级别。如果propagate_on_container_move_assignment::valueis_nothrow_move_assignable&lt;allocator_type&gt;::value 都为真,则可以将移动赋值运算符标记为noexcept。而且我只会说 不知道一个相当简单的已知配方,它利用了在其他方法中实现的功能。 :-) 其他人可能会。我的理念是对每一位特殊的成员给予单独的关爱,并以= default作为第一选择来实施每一位。
    猜你喜欢
    • 2011-02-27
    • 2021-07-30
    • 2019-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-28
    • 2011-12-04
    • 2011-01-07
    相关资源
    最近更新 更多