在本文中,请看一看多态分配器,并了解如何调试源和自定义类型。
在我先前关于多态分配器的文章中,我们讨论了一些基本思想。例如,您已经看到一个使用单调资源pmr::vector
保存的pmr::string
。在这样的容器中使用自定义类型怎么样?如何启用?让我们来看看。
目标
在上一篇文章中,有类似的代码:
字符 缓冲区[ 256 ] = {}; //堆栈上的一个小缓冲区
STD :: fill_n(STD ::开始(缓冲器),的std ::尺寸(缓冲区)- 1,'_');
std :: pmr :: monotonic_buffer_resource 池{ std :: data(buffer),
std :: size(buffer)};
std :: pmr ::向量< std :: pmr ::字符串> vec { &pool };
// ...
查看完整示例@Coliru
在这种情况下,当您在向量中插入新字符串时,新对象还将使用向量中指定的内存资源。
“使用”是指字符串对象必须分配一些内存的情况,这意味着长字符串不适合“短字符串优化”缓冲区。如果该对象不需要获取任何额外的内存块,则它只是父向量的连续内存博客的一部分。
由于pmr::string
可以使用向量的内存资源,因此这意味着它以某种方式“知道”分配器。
如何编写自定义类型:
结构 产品{
std ::字符串 名称;
字符 费用{ 0 }; //为简单起见
};
如果我将此插入向量:
std :: pmr :: vector <产品> 产品{和池};
然后,向量将使用提供的内存资源,但不会将其传播到中Product
。这样,如果Product
必须为其分配内存,name
则将使用默认分配器。
我们必须“启用”我们的类型并使它知道分配器,以便它可以利用父容器中的分配器。
参考文献
在开始之前,如果您想自己尝试使用分配器,我想提到一些很好的参考。这个主题不是很流行,因此查找教程或好的说明并不像我发现的那么容易。
- CppCon 2017:Pablo Halpern“分配器:好的部分”-YouTube-分配器和PMR新内容的深入说明。即使使用一些基于节点的容器的测试实现。
- CppCon 2015:YouTube的Andrei Alexandrescu“ std :: allocator…”-YouTube-从介绍中可以学到
std::allocator
解决远近问题并使之保持一致的意图,但是现在我们希望从该系统中获得更多。 - 在C ++ 0x中allocator_traits的用途是什么?- 堆栈溢出
- Jean Guegant的博客–从零开始制作与STL兼容的哈希图-第3部分-迭代器和分配器的奇妙世界-这是一篇有关如何更多地使用分配器的超级详细的博客文章,更不用说好的轶事和笑话了:)
- 感谢您的内存(分配器) -Sticky Bits-对分配器,它们的故事以及PMR新模型如何适用的有价值的介绍。您还可以了解如何编写跟踪pmr分配器以及如何
*_pool_resource
工作。 - CppCon 2018:Arthur O’Dwyer,“分配者是堆的把手” – Arthur的精彩演讲,他分享了理解分配器所需的全部知识。
- C ++ 17- Nicolai Josuttis撰写的《完整指南》 -书中有一长篇关于PMR分配器的章节。
调试内存资源
为了有效地使用分配器,拥有一个允许我们跟踪容器中的内存分配的工具将很方便。
请参阅我列出的如何执行操作的资源,但是以基本形式,我们必须执行以下操作:
- 从获得
std::pmr::memory_resource
- 实行:
do_allocate()
-用于以给定对齐方式分配N个字节的函数。do_deallocate()
-当对象想要释放内存时调用的函数。do_is_equal()
-它用于比较两个对象是否具有相同的分配器,在大多数情况下,您可以比较地址,但是如果使用某些分配器适配器,则可能需要查看一些高级教程。
- 将您的自定义内存资源设置为对对象和容器有效。
这是基于Sticky Bits和Pablo Halpern的演讲的代码。
class debug_resource:public std :: pmr :: memory_resource {
公开:
明确的 debug_resource(std ::字符串 名称,
std :: pmr :: memory_resource * up = std :: pmr :: get_default_resource())
:_name { std :: move(name)},_upstream { up }
{}
void * do_allocate(size_t 字节,size_t 对齐)覆盖{
std :: cout << _name << “ do_allocate():” << 字节 << '\ n' ;
无效* ret = _upstream- >分配(字节,对齐);
返回 ret ;
}
void do_deallocate(void * ptr,size_t 字节,size_t 对齐)覆盖{
std :: cout << _name << “ do_deallocate():” << 字节 << '\ n' ;
_upstream- >释放(ptr,bytes,alignment);
}
bool do_is_equal(const std :: pmr :: memory_resource & other)const noexcept 覆盖{
返回 this == &other ;
}
私人的:
std ::字符串 _name ;
std :: pmr :: memory_resource * _upstream ;
};
调试资源只是实际内存资源的包装。如您在分配/解除分配函数中看到的,我们只记录数字,然后将实际作业推迟到上游资源。
用例示例:
constexpr size_t BUF_SIZE = 128 ;
字符 缓冲区[ BUF_SIZE ] = {}; //堆栈上的一个小缓冲区
STD :: fill_n(STD ::开始(缓冲器),的std ::尺寸(缓冲区)- 1,'_');
debug_resource default_dbg { “ default” };
std :: pmr :: monotonic_buffer_resource 池{ std :: data(buffer),std :: size(buffer),和default_dbg };
debug_resource dbg { “ pool”,&pool };
std :: pmr ::向量< std ::字符串> 字符串{ &dbg };
弦。emplace_back(“ Hello Short String”);
弦。emplace_back(“ Hello Short String 2”);
输出:
池 do_allocate():32
池 do_allocate():64
池 do_deallocate():32
池 do_deallocate():64
上面我们两次使用了调试资源,第一个"pool"
用于记录请求到的每个分配monotonic_buffer_resource
。在输出中,您可以看到我们有两个分配和两个释放。
还有另一个调试资源"default"
。它被配置为单调缓冲区的父级。这意味着如果pool
需要分配。,那么它必须通过我们的"default"
对象来请求内存。
如果您添加三个字符串,例如:
弦。emplace_back(“ Hello Short String”);
弦。emplace_back(“ Hello Short String 2”);
弦。emplace_back(“你好,字符串更长”);
然后输出是不同的:
池 do_allocate():32
池 do_allocate():64
池 do_deallocate():32
池 do_allocate():128
默认 do_allocate():256
池 do_deallocate():64
池 do_deallocate():128
默认的 do_deallocate():256
这次您可以注意到,对于第三个字符串,我们预定义的小缓冲区内没有空间,这就是为什么单调资源不得不要求“默认”另外256个字节的原因。
在此处查看完整代码@Coliru。
自定义类型
配备了调试资源以及一些“缓冲区打印技术”,我们现在可以检查我们的自定义类型是否可与分配器一起使用。让我们来看看:
struct SimpleProduct {
std ::字符串 _name ;
char _price { 0 };
};
int main(){
constexpr size_t BUF_SIZE = 256 ;
字符 缓冲区[ BUF_SIZE ] = {}; //堆栈上的一个小缓冲区
STD :: fill_n(STD ::开始(缓冲器),的std ::尺寸(缓冲区)- 1,'_');
常量 自动 BufferPrinter = [](STD :: string_view BUF,的std :: string_view 标题){
std :: cout << 标题 << “:\ n” ;
为(为size_t 我 = 0 ;我 < BUF。大小(); ++我){
std :: cout <<(buf [ i ] > = '' ? buf [ i ]:'#');
如果((i + 1)%64 == 0)std :: cout << '\ n' ;
}
std :: cout << '\ n' ;
};
BufferPrinter(buffer,“ initial buffer”);
debug_resource default_dbg { “ default” };
std :: pmr :: monotonic_buffer_resource 池{ std :: data(buffer),std :: size(buffer),和default_dbg };
debug_resource dbg { “ buffer”,&pool };
std :: pmr :: vector < SimpleProduct > 产品{ &dbg };
产品。储备金(4);
产品。emplace_back(SimpleProduct { “ car”,'7' });
产品。emplace_back(SimpleProduct { “ TV”,'9' });
产品。emplace_back(SimpleProduct { “产品名称再延长一点,'4' });
BufferPrinter(STD :: string_view {缓冲器,BUF_SIZE },“之后插入”);
}
可能的输出:
________________________________________________________________
________________________________________________________________
________________________________________________________________
________________________________________________________________
缓冲区 do_allocate():160
插入后:
p “ --- • ..-....... car.er ..-〜--- • ..7 _______-” --- • .. - ....... TV .. er ..
- 〜--- • .. 9_______0 - Ĵ - ...... - ...... - ...... ________4_______________
________________________________________________________________
_______________________________________________________________。
缓冲区 do_deallocate():160
图例:在输出中,点.
表示缓冲区的元素为0
。不是零但小于空格32的值显示为-
。
让我们解密代码和输出:
向量包含的SimpleProduct
对象只是一个字符串和一个数字。我们保留了四个元素,您会注意到我们的调试资源记录了160个字节的分配。插入三个元素之后,我们可以发现car
和数字7
(这就是为什么我将其char
用作价格类型)。然后TV
用9
。我们也可以注意到4
第三个元素的价格,但是那里没有名字。这意味着它被分配到其他地方。
实时代码@Coliru
分配器感知类型
知道自定义类型分配器并不困难,但是我们必须记住以下几点:
pmr::*
尽可能使用类型,以便您可以将它们传递给分配器。- 声明,
allocator_type
以便分配器特征可以“识别”您的类型使用分配器。您还可以为分配器特征声明其他属性,但是在大多数情况下,默认设置就可以了。 - 声明采用分配器的构造函数,并将其进一步传递给您的成员。
- 声明复制并移动构造器,该构造器还负责分配器。
- 与分配和移动操作相同。
这意味着我们相对简单的自定义类型声明必须增加:
结构 产品{
使用 allocator_type = std :: pmr :: polymorphic_allocator < char > ;
显式 产品(allocator_type alloc = {})
:_name { alloc } {}
产品(std :: pmr ::字符串 名称,字符 价格,
const allocator_type和 alloc = {})
:_name { std :: move(name),alloc },_price { price } {}
产品(const Product & other,const allocator_type & alloc)
:_name {其他。_name,alloc },_price { other。_price } {}
产品(产品&& 其他,const allocator_type和 alloc)
:_name {性病::移动(其他。_name),ALLOC },_price {其它。_price } {}
产品和 运营商=(常量 产品和 其他)= 默认;
产品& 运营商=(产品&& 其他)= 默认值;
std :: pmr ::字符串 _name ;
char _price { '0' };
};
这是一个示例测试代码:
debug_resource default_dbg { “ default” };
std :: pmr :: monotonic_buffer_resource 池{ std :: data(buffer),
std :: size(buffer),&default_dbg };
debug_resource dbg { “ buffer”,&pool };
std :: pmr :: vector <产品> 产品{ &dbg };
产品。储备金(3);
产品。emplace_back(产品{ “汽车”,'7' ,& DBG });
产品。emplace_back(产品{ “TV” , '9' ,& DBG });
产品。emplace_back(产品{ “多一点的时间商品名”,'4' ,& DBG });
输出:
缓冲区 do_allocate():144
缓冲区 do_allocate():26
插入后:
----- • .. ----- • .. - .......汽车。#• .. - ....... 7_______ ----- • .. ----- • ..
- .......电视.. #• .. - ....... 9_______ ----- • .. ---- • .. - ....... - 。......
________4_______ 产品名称更长一点 。______________________
_______________________________________________________________。
缓冲区 do_deallocate():26
缓冲区 do_deallocate():144
示例代码@Coliru
在输出中,第一个内存分配144是给的vector.reserve(3)
,然后我们为另一个较长的字符串(第三个元素)分配了另一个。还打印了完整的缓冲区(Coliru链接中可用的代码),该缓冲区显示了字符串所在的位置。
“全”自定义容器
我们的自定义对象由其他pmr::
容器组成,因此更加简单!而且我想在大多数情况下,您可以利用现有类型。但是,如果您需要访问分配器并执行自定义内存分配,那么您应该看到Pablo的演讲,他在其中指导了一个自定义列表容器的示例。
CppCon 2017:Pablo Halpern“分配者:好的部分”-YouTube
概要
在此博客文章中,我们在标准库的更深层次内进行了另一次旅程。尽管分配器令人恐惧,但使用多态分配器似乎使事情变得更加舒适。如果您坚持使用pmr::
命名空间中公开的许多标准容器,则会发生这种情况。
让我知道您对分配器和pmr::
东西有什么经验。也许您以不同的方式实现您的类型?(我尝试编写正确的代码,但仍然有些细微之处是棘手的。让我们一起学习一些知识吧:) 福州APP开发