c++11 - 新的现代 C++ 容器中的分配器传播策略

标签 c++11 move-semantics allocator copy-assignment move-assignment-operator

将这些特征放在容器中的原因是什么 (https://en.cppreference.com/w/cpp/memory/allocator_traits)

propagate_on_container_copy_assignment  Alloc::propagate_on_container_copy_assignment if present, otherwise std::false_type
propagate_on_container_move_assignment  Alloc::propagate_on_container_move_assignment if present, otherwise std::false_type
propagate_on_container_swap             Alloc::propagate_on_container_swap if present, otherwise std::false_type
is_always_equal(since C++17)            Alloc::is_always_equal if present, otherwise std::is_empty<Alloc>::type
我知道容器实现在分配和交换的实现中将以一种或另一种方式表现。 (并且处理这些情况是可怕的代码。)
我也了解有时可能需要将 move 容器置于 resizeble 的状态。或者至少可以调用最后一次释放,因此分配器不能无效。 (我个人认为这是一个弱论点。)
但问题是,为什么这些信息不能成为自定义分配器类型本身的正常实现和语义的一部分?
我的意思是,容器复制分配可以尝试复制分配源分配器,如果该语法复制分配没有真正复制,那么,就像说你的容器没有 propagate_on_container_copy_assignment .
以同样的方式,而不是使用 is_always_equal实际上可以使分配器分配什么也不做。
(此外,如果 is_always_equal 为真,则可以使 operator== 分配器返回 std::true_type 以发出信号。)
在我看来,这些特征似乎试图覆盖人们可以通过普通 C++ 手段赋予自定义分配器的语义。
这似乎与泛型编程和当前的 C++ 哲学背道而驰。
唯一的原因,我认为这对于实现与“旧”容器的某种向后兼容性很有用。
如果我今天要编写一个新容器和/或一个新的非平凡分配器,我可以依赖分配器的语义而忘记这些特征吗?
在我看来,只要移出的分配器可以“解除分配”一个空指针状态(这意味着在这种特殊情况下基本上什么都不做),那么它应该没问题,如果 resize抛出,这也很好(有效),它只是意味着分配器不再有权访问它的堆。

编辑:实际上,我可以这样写容器吗 ?并将复杂性委托(delegate)给自定义分配器的语义?:
templata<class Allocator>
struct my_container{
  Allocator alloc_;
  ...
  my_container& operator=(my_container const& other){
    alloc_ = other.alloc_; // if allocator is_always_equal equal this is ok, if allocator shouldn't propagate on copy, Alloc::operator=(Alloc const&) simply shouldn't do anything in the first place
    ... handle copy...
    return *this;
  }
  my_container& operator=(my_container&& other){
    alloc_ = std::move(other.alloc_); // if allocator shouldn't propagate on move then Alloc::operator=(Alloc&&) simply shouldn't do anything.
    ... handle move...
    return *this;
  }
  void swap(my_container& other){
     using std::swap;
     swap(alloc, other.alloc); //again, we assume that this does the correct thing (including not actually swapping anything if that is the desired criteria. (that would be the case equivalent to `propagate_on_container_swap==std::false_type`)
     ... handle swap...
  }
}
我认为对分配器的唯一真正要求是,从分配器移出的分配器应该能够做到这一点。
my_allocator a2(std::move(a1));
a1.deallocate(nullptr, 0); // should ok, so moved-from container is destructed (without exception)
a1.allocate(n); // well defined behavior, (including possibly throwing bad_alloc).
并且,如果移出容器由于移出分配器失去对堆的访问权而无法调整大小(例如,因为没有特定资源的默认分配器),那么太糟糕了,那么操作将抛出(因为任何调整大小可以扔)。

最佳答案

尼可波拉斯的回答非常准确。我会这样说:

  • An allocator is a handle to a heap.它是一种值语义类型,就像指针或 intstring .当你复制一个分配器时,你会得到一个它的值的副本。副本比较相等。这适用于分配器,就像它适用于指针或 int s 或 string s。
  • 使用分配器可以做的一件事是使用纯值语义将其传递给不同的算法和数据结构。 STL 在这个部门没有太多,但它确实有例如。 allocate_shared .
  • 您可以使用分配器做的另一件事是将其提供给 STL 容器。您在容器构建期间将分配器分配给容器。在其生命周期的某些时刻,容器会遇到其他分配器,它必须做出选择。

  • A<int> originalAlloc = ...;
    std::vector<int, A<int>> johnny(originalAlloc);
    
    A<int> strangeAlloc = ...;
    std::vector<int, A<int>> pusher(strangeAlloc);
    
    // pssst kid wanna try my allocator? it'll make you feel good
    johnny = std::move(pusher);
    

    此时,johnny不得不做出一个艰难的决定:“就我的值(value)而言,我正在采用 pusher 的元素值;我是否也应该采用他的分配器?”

    一路johnny在 C++11 及更高版本中,他的决定是咨询 allocator_traits<A<int>>::propagate_on_container_move_assignment照它说的去做:如果它说true那么我们将采用strangeAlloc , 如果它说 false我们将坚持我们的原则并坚持我们最初的分配器。坚持使用我们原来的分配器确实意味着我们可能需要做很多额外的工作来复制所有 pusher的元素(我们不能只是窃取他的数据指针,因为它指向与 strangeAlloc 关联的堆,而不是与 originalAlloc 关联的堆)。

    关键是,决定坚持使用当前分配器还是采用新分配器是一个仅在容器上下文中才有意义的决定。这就是为什么特征 propagate_on_container_move_assignment (POCMA) 和 POCCA 和 POCS 的名称中都有“容器”。这是关于容器分配期间发生的事情,而不是分配器分配。分配器赋值遵循值语义,因为分配器是值语义类型。时期。

    所以,应该propagate_on_container_move_assignment (POCMA) 和 POCCA 和 POCS 都已经是容器类型的属性了吗?我们应该有std::vector<int>混杂地采用分配器,和 std::stickyvector<int>总是坚持使用它构建的分配器?我们可能会。

    C++17 种假装我们确实是这样做的,通过提供像 std::pmr::vector<int> 这样的 typedef看起来与 std::stickyvector<int> 非常相似;但在引擎盖下std::pmr::vector<int>只是 std::vector<int, std::pmr::polymorphic_allocator<int>> 的 typedef并且仍然通过咨询 std::allocator_traits<std::pmr::polymorphic_allocator<int>> 弄清楚该怎么做.

    关于c++11 - 新的现代 C++ 容器中的分配器传播策略,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54703727/

    相关文章:

    c++ - 为什么 STL 容器中的交换成员函数没有声明为 noexcept?

    c++ - 我可以依赖无序 map 的顺序吗?

    c++ - 锁定线程安全队列的 move 构造函数的右值参数?

    c++ - 将可修改右值引用绑定(bind)到可修改左值

    c++ - 在基于范围的 for 循环中使用转发引用有什么好处?

    c++ - 如何使用分配器精确模拟新的 T[n]?

    c++ - 嵌套容器的自定义分配器

    c++ - 有趣的用例,其中常规数组不能代替 std::array

    c++ - 如何获得 boost SPSC 队列的大小?

    c++ - C++: vector 分配器行为,内存分配和智能指针