如何正确地创建一个同时使用C11和C17多态分配器的容器?以下是我目前所拥有的(作为通用样板模板):
说明:我已经包含了两个字段,res_
显示了如何直接从容器管理动态内存,而字段vec_
用于演示分配器如何向下传播。我从巴勃罗Halpern的演讲Allocators: The Good Parts中学到了很多,但他主要谈论pmr分配器,而不是c++11。
演示
#include <cstdio>
#include <vector>
#include <memory>
#include <memory_resource>
template <typename T, typename Allocator = std::allocator<T>>
struct MyContainer {
auto get_allocator() const -> Allocator {
return vec_.get_allocator();
}
MyContainer(Allocator allocator = {})
: vec_{ allocator }
{}
MyContainer(T val, Allocator allocator = {})
: MyContainer(allocator)
{
res_ = std::allocator_traits<Allocator>::allocate(allocator, sizeof(T));
std::allocator_traits<Allocator>::construct(allocator, res_, std::move(val));
}
~MyContainer() {
Allocator allocator = get_allocator();
std::allocator_traits<Allocator>::destroy(allocator, std::addressof(res_));
std::allocator_traits<Allocator>::deallocate(allocator, res_, sizeof(T));
res_ = nullptr;
}
MyContainer(const MyContainer& other, Allocator allocator = {})
: MyContainer(allocator)
{
operator=(other);
}
MyContainer(MyContainer&& other) noexcept
: MyContainer(other.get_allocator())
{
operator=(std::move(other));
}
MyContainer(MyContainer&& other, Allocator allocator = {})
: MyContainer(allocator)
{
operator=(std::move(other));
}
auto operator=(MyContainer&& other) -> MyContainer& {
if (other.get_allocator() == get_allocator()) {
std::swap(*this, other);
} else {
operator=(other); // Copy assign
}
}
auto operator=(const MyContainer& other) -> MyContainer& {
if (other != this) {
std::allocator_traits<Allocator>::construct(get_allocator(), std::addressof(vec_), vec_);
std::allocator_traits<Allocator>::construct(get_allocator(), std::addressof(res_), other);
}
return *this;
}
private:
std::vector<T, Allocator> vec_; // Propagation
T* res_ = nullptr;
};
int main() {
MyContainer<std::string, std::pmr::polymorphic_allocator<std::byte>> ctr1 = std::string{"Hello World!"};
MyContainer<double> ctr2 = 2.5;
}
然而,即使这样也不能按计划工作,因为vector期望其值类型与分配器的值类型相匹配:
<source>:67:31: required from 'struct MyContainer<std::__cxx11::basic_string<char>, std::pmr::polymorphic_allocator<std::byte> >'
<source>:72:74: required from here
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/stl_vector.h:438:64: error: static assertion failed: std::vector must have the same value_type as its allocator
438 | static_assert(is_same<typename _Alloc::value_type, _Tp>::value,
|
我还漏掉了什么我是否应该根据分配器的传播特性进行不同的传播(通用容器需要这样做吗)?
2条答案
按热度按时间eulz3vhy1#
tl;dr
value_type
与容器的value_type
相同;否则它将是病态的。因此在这种情况下,需要为
MyContainer
使用std::pmr::polymorphic_allocator<std::string>
,或者在将分配器类型传递给std::vector
之前重新绑定它,例如:std::polymorphic_allocator
和std::allocator
的容器类相对容易--它们都满足指定的需求Allocator,所以在这种情况下所需要的特殊酱料只是不做任何特殊的事情--将它们实现为一个标准的分配器(基本上使用std::allocator_traits<Alloc>
来处理与分配器的所有交互)1.为什么给出的代码示例格式不正确
所有为allocator-aware container的容器必须有一个分配器,其
value_type
与容器的value_type
相同。本标准规定:(强调我的)
24.2.2.5 Allocator-aware containers(4)
(3)在本款中,
(3.1)-
X
表示具有T
的value_type
的分配器感知容器类,其使用A
类型的分配器,[...]如果
X
满足容器要求,并且以下类型、语句和表达式格式良好并具有指定的语义,则类型X
满足可识别分配器的容器要求。typename X::allocator_type
A
allocator_type::value_type
与X::value_type
相同。所以下面的语句对于allocator-aware容器必须始终为真:
请注意,标准库中定义的所有容器(
std::array
除外)都必须是分配器感知的。(参见24.2.2.5(1)Allocator-aware容器)请注意,在您的示例中,该语句将不满足:
std::vector
不是一个能够识别分配器的容器(因为它不满足这个要求)std::vector
必须是一个能够识别分配器的容器=>由于标准中的矛盾,这是病态的。
请注意,这也与您从gcc获得的错误消息相匹配:
2.为什么链接视频没有问题
您在评论中链接的Youtube Video(CppCon 2017:巴勃罗Halpern“分配器:The Good Parts”)是关于一个不使用任何标准库容器的用户定义的容器类。
标准没有对用户定义的容器类型强加任何规则,因此基本上可以在那里做任何想做的事情。
以下是这堂课的一小段文字记录:
注意,
allocator_type
被硬编码为std::pmr::polymorphic_allocator<std::byte>
,因此allocator_type::value_type
通常不会匹配slist::value_type
(除了两者都是std::byte
的情况);所以这个容器在大多数情况下不能满足allocator-aware container的要求。
但也没有要求它这样做。
=>格式良好
注意:如果一个人通过了,那将是病态的。
slist<>
到一个函数,该函数要求其参数必须是一个分配器感知的容器。- 但是只要避免定义几乎一致的容器就没有问题。3.如何写一个可以和任何分配器一起工作的容器
注意,
std::pmr::polymorphic_allocator
满足指定的需求Allocator,就像std::allocator
一样。(All用于标准容器的分配器必须满足该要求)
因此,支持这两种分配器的技巧就是不做任何特殊的事情--像对待任何其他分配器一样对待
std::pmr::polymorphic_allocator
,因为它就是这样。(基本上所有内容都使用std::allocator_traits<Alloc>
)请注意,这也意味着您应该遵守
std::allocator_traits<Allocator>::propagate_on_
container_copy
/container_move_assignment
/container_swap
值。这对于
polymorphic_allocator
意味着分配器在复制/移动/交换容器时不应该**传播。因为这样做可能会导致令人惊讶的生命周期问题-例如参见this answer。
(Of当然,应该始终尊重这些,而不仅仅是
polymorphic_allocator
s)oxf4rvwz2#
我花了最后一天的时间收集了所有关于分配器的资源,并提出了一个通用的设计。我会把它写在这里,也许有人会发现它很有用。
泛型类stl分配器感知容器实现
演示
注意事项:
一般形状:
std::allocator_traits<Allocator>
来完成。你永远不能直接使用allocator->allocate(),因为std::allocator_traits
甚至为std::allocator<T>
(默认分配器)提供了默认值,这些默认值在类型中不一定可用!get_alloc_()
中的static_cast可检索;分配器传播:
不幸的是,分配器有一个自定义点,允许它们在三个操作上传播到其他容器移动,复制和交换,事后看来这只会造成伤害,但如果我们想要符合stl,我们需要考虑这些情况:
propagate_on_container_move_assignment::value
为true,则B.allocator将被移动分配给a.allocatorpropagate_on_container_copy_assignment::value
为true,则B.allocator将被复制到a.allocatorpropagate_on_container_swap::value
为true,则a.allocator将与b.allocator交换构造函数
移动赋值
看看霍华德·欣南特的精彩answer在这里。
alloc_traits::propagate_on_container_move_assignment::value
为true),这是没有问题的,我们可以移动。副本分配
alloc_traits::propagate_on_container_copy_assignment::value
的计算结果为true,则首先复制分配器。swap
alloc_traits::propagate_on_container_swap
)上传播时,才有可能真正交换。noexcept
propagate_on_container_move_assignment::value
和is_nothrow_move_assignable<allocator_type>::value
的计算结果都为true时,移动赋值运算符才可以标记为noexcept(因为分配器也被移动到了case中的新对象)。noexcept
,因为它分配和构造新元素,即请求系统内存并调用构造函数。理论上只有当容器仍然为空时才可以是noexcept,但C++不支持运行时检查。noexcept
移动元素,因此我们包括一个扩展的移动ctor,它不能保证,因为它需要一个分配器参数离开默认的移动ctornoexcept
。扩展的移动对象可以是no,除非分配器比较相等,但C++不支持运行时检查。异常安全,copy & swap:
move_assign_()
-方法中不包括分配器的字段,仍然可以实现复制和交换。未解决问题:
也许有人能帮我回答最后几个问题!
版本1
我添加了改变了的东西,浮现在脑海中:
注意,函数上的noexcept规范不是编译时检查;它仅仅是程序员通知编译器函数是否应该抛出异常的一种方法。
free_res_()
函数并增加了异常安全性。我将不得不再次检查,但我认为分配现在有很强的异常安全性。no_unique_address
作为一种更优雅的方式来处理空分配器类型的分配器存储。来源: