alf*_*lfC 3 allocator move-semantics copy-assignment c++11 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
Run Code Online (Sandbox Code Playgroud)
is_always_equal(since C++17) Alloc::is_always_equal if present, otherwise std::is_empty<Alloc>::type
Run Code Online (Sandbox Code Playgroud)
我知道容器实现在分配和交换的实现中会以一种或另一种方式表现。(并且处理这些情况是可怕的代码。)我也明白有时人们可能需要将移动容器保持在一种状态resizeble或者至少可以调用一些最后的释放,因此分配器不能无效。(我个人认为这是一个弱论点。)
但问题是, 为什么这些信息不能成为自定义分配器类型本身的正常实现和语义的一部分?
我的意思是,容器复制分配可以尝试复制分配源分配器,如果语法复制分配没有真正复制,那么,就像说你的容器没有 propagate_on_container_copy_assignment。
以同样的方式而不是使用 is_always_equal一个实际上可以使分配器分配什么也不做。
(此外,如果is_always_equal为真,则可以让operator==分配器返回std::true_type以发出信号。)
在我看来,这些特征似乎试图覆盖可以通过普通 C++ 方式提供给自定义分配器的语义。这似乎与泛型编程和当前的 C++ 哲学背道而驰。
唯一的原因,我认为这对于实现与“旧”容器的某种向后兼容性很有用。
如果我今天要编写一个新容器和/或一个新的非平凡分配器,我可以依靠分配器的语义而忘记这些特征吗?
在我看来,只要移动的分配器可以“解除分配”一个空指针状态(这意味着在这种特殊情况下主要是什么都不做),那么它应该没问题,如果resize抛出,那也很好(有效) ,这只是意味着分配器无法再访问其堆。
编辑:实际上, 我可以这样简单地编写容器吗?并将复杂性委托给自定义分配器的语义?:
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...
}
}
Run Code Online (Sandbox Code Playgroud)
我认为对分配器的唯一真正要求是,移动的分配器应该能够做到这一点。
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).
Run Code Online (Sandbox Code Playgroud)
而且,如果移出的容器因为移出的分配器无法访问堆而无法调整大小(例如,因为特定资源没有默认分配器),那么太糟糕了,那么操作将抛出(任何调整大小可以扔)。
Nicol Bolas 的回答非常准确。我会这样说:
分配器是堆的句柄。它是一个值语义类型,就像一个指针或一个int或一个string。当您复制分配器时,您将获得其值的副本。副本比较相等。这适用于分配器,就像它适用于指针或ints 或strings 一样。
您可以使用分配器做的一件事是使用纯值语义将它传递给不同的算法和数据结构。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);
Run Code Online (Sandbox Code Playgroud)
此时,johnny不得不做出艰难的决定:“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>which 混杂采用分配器,并且std::stickyvector<int>始终坚持使用它构建的分配器?我们很可能会。
C++17 假装我们确实是那样做的,通过给这样的 typedefstd::pmr::vector<int>看起来非常类似于std::stickyvector<int>; 但在引擎盖std::pmr::vector<int>下只是一个 typedefstd::vector<int, std::pmr::polymorphic_allocator<int>>并且仍然通过咨询来弄清楚要做什么std::allocator_traits<std::pmr::polymorphic_allocator<int>>。
我的意思是,容器复制分配可以尝试复制分配源分配器,如果语法复制分配没有真正复制,那么,就像说你的容器没有 复制
propagate_on_container_copy_assignment。
概念/命名要求“ CopyAssignable ”意味着不仅仅是将左值分配给与该左值相同类型的对象的能力。它也具有语义含义:预期目标对象的价值与原始对象的价值相同。如果您的类型提供复制赋值运算符,则期望该运算符复制对象。标准库中几乎所有允许复制分配的东西都需要这样做。
如果你给标准库一个类型,它需要是 CopyAssignable,并且它有一个不遵守该概念/命名要求的语义含义的复制赋值运算符,未定义的行为结果。
分配器具有某种“值”。并复制分配器复制该“值”。在复制/移动/交换上传播的问题基本上是在问这个问题:分配器的值是容器值的一部分吗?这个问题只在处理容器的范围内被提出;在处理一般的分配器时,这个问题没有实际意义。分配器有一个值,复制它会复制该值。但这相对于先前分配的存储意味着什么是一个完全独立的问题。
因此具有特征。
如果我今天要编写一个新容器和/或一个新的非平凡分配器,我可以依靠分配器的语义而忘记这些特征吗?
...
我可以这样简单地编写容器吗?并将复杂性委托给自定义分配器的语义?:
违反AllocatorAwareContainer规则的容器不是分配器感知容器,您不能合理地将遵循标准库分配器模型的分配器传递给它。类似地,违反分配器规则的分配器不能合理地分配给 AllocatorAwareContainer,因为该模型要求分配器实际上是分配器。这包括句法和语义规则。
如果您不为propagate_on_*属性提供值,则将使用默认值false。这意味着它不会尝试传播您的分配器,因此您不会违反分配器可复制/移动分配或可交换的需求。然而,这也意味着你的分配器的复制/移动/交换行为永远不会被使用,所以你给这些操作提供什么语义并不重要。此外,在没有传播的情况下,如果两个分配器不相等,则意味着线性时间移动/交换。
但是,仍然不允许 AllocatorAwareContainer忽略这些属性,因为根据定义,它必须实现它们才能承担该角色。如果分配器定义了复制赋值运算符,但将其在复制时传播设为 false(这是完全有效的代码),则在对容器进行复制分配时不能调用分配器的复制赋值运算符。
基本上,使这项工作有效的唯一方法是生活在您自己的世界中,在那里您只将“容器”与“分配器”一起使用,而永远不要尝试将任何标准库等价物与“容器/分配器”一起使用。
历史回顾也有帮助。
出于历史目的,这些propagate_on_*功能在 C++11 中有着曲折的历史,但它从未像您建议的那样出现。
我能找到的关于这个主题的最早论文是N2525 (PDF): Allocator-specific Swap and Move Behavior。这种机制的主要意图是允许某些类别的有状态迭代器能够进行恒定时间移动和交换操作。
这曾一度被一个基于概念的版本所包含,但是一旦从 C++0x 中删除,它就会以新名称和更简化的界面 (PDF)(是的,您的界面)返回到traits 类。现在重新使用的是简单版本。不客气;))。
因此,在所有情况下,都明确承认复制/移动/交换的存在与这些操作对容器的意义之间需要有区别。
并且处理这些情况是可怕的代码。
但事实并非如此。在 C++17 中,您只需使用if constexpr. 在旧版本中,您必须依赖 SFINAE,但这仅意味着编写如下函数:
template<typename Alloc>
std::enable_if_t<std::allocator_traits<Alloc>::propagate_on_container_copy_assignment::value> copy_assign_allocator(Alloc &dst, const Alloc &src)
{
dst = src;
}
template<typename Alloc>
std::enable_if_t<!std::allocator_traits<Alloc>::propagate_on_container_copy_assignment::value> copy_assign_allocator(Alloc &dst, const Alloc &src) {}
Run Code Online (Sandbox Code Playgroud)
随着移动和交换版本。然后根据传播行为调用该函数来执行复制/移动/交换或不执行复制/移动/交换。
| 归档时间: |
|
| 查看次数: |
1246 次 |
| 最近记录: |