STL コンテナの要件って言っても、実際は仕様的なものより、コンテナのデータ構造やアルゴリズムを知るだけで十分です。
例えば以下のような点さえ掴んでおけば、アロケーターが使うメモリー消費量を把握しやすくなります。
std::vector のサポートはそれほどは難しくないですが、リサイズを頻繁に行うためデバッグが面倒となりがちです。
std::list は双方向連結リストのポインターのために 16 バイト、データとパディングを合わせると 24 バイトは最低でも無いと割り当てはできないはずです。
さらに std::map は赤黒木というアルゴリズムを使うため、そのためのデータ領域が必要となります。
sizeof(std::_Rb_node_tree_base) にデータ用バイトサイズを加えたサイズを割り当てないと駄目です。
ではとりあえず std::map に適合するヒープアロケーターを作ってみましょう。
main.cpp.
1 #include <iostream>
2 #include <new>
3 #include <cstdlib>
4 #include <cstring>
5 #include <map>
6 #include <vector>
7 #include <list>
8
9 #define MAX_OBJECTS 64
10
11 template<typename T>
12 struct is_pair : std::false_type {};
13
14 template<typename T, typename U>
15 struct is_pair<std::pair<const T,U>> {
16 static constexpr bool value = true;
17 };
18
19 template<typename T, typename U>
20 struct is_pair<std::_Rb_tree_node<std::pair<const T,U>>> {
21 static constexpr bool value = true;
22 };
23
24 template<typename T>
25 struct MapAllocator {
26 using value_type = T;
27 using pointer = T*;
28 using const_pointer = const T*;
29 using reference = T&;
30 using const_reference = const T&;
31 using size_type = std::size_t;
32
33 using size_addr_t = std::map<void*,size_type>;
34 using free_mem_t = std::map<size_type,std::vector<void*>>;
35
36 static_assert(is_pair<T>::value);
37
38 unsigned M_count_;
39 const unsigned M_block_size_;
40 size_addr_t M_size_map_;
41 free_mem_t M_free_map_;
42 char* Mp_pool_on_heap_;
43
44 template<typename U>
45 struct rebind
46 {
47 typedef MapAllocator<U> other;
48 };
49
50 inline MapAllocator() noexcept :
51 M_count_(0),
52 M_block_size_(sizeof(T)+sizeof(std::_Rb_tree_node_base))
53 {
54 Mp_pool_on_heap_ = static_cast<char*>(std::malloc(MAX_OBJECTS*M_block_size_));
55 std::cout << "M_block_size_ = " << M_block_size_ << '\n';
56 }
57
58 inline constexpr MapAllocator(const MapAllocator& a) noexcept
59 : M_count_(a.M_count_),
60 M_block_size_(a.M_block_size_),
61 M_size_map_(a.M_size_map_),
62 M_free_map_(a.M_free_map_)
63 {
64 Mp_pool_on_heap_ = static_cast<char*>(std::malloc(MAX_OBJECTS*M_block_size_));
65 std::memcpy(Mp_pool_on_heap_,a.Mp_pool_on_heap_,MAX_OBJECTS*M_block_size_);
66 }
67
68 template<typename U>
69 inline constexpr MapAllocator(const MapAllocator<U>& a) noexcept
70 : M_count_(a.M_count_),
71 M_block_size_(a.M_block_size_),
72 M_size_map_(a.M_size_map_),
73 M_free_map_(a.M_free_map_)
74 {
75 Mp_pool_on_heap_ = static_cast<char*>(std::malloc(MAX_OBJECTS*M_block_size_));
76 std::memcpy(Mp_pool_on_heap_,a.Mp_pool_on_heap_,MAX_OBJECTS*M_block_size_);
77 }
78
79 inline ~MapAllocator(){
80 std::free(Mp_pool_on_heap_);
81 Mp_pool_on_heap_ = nullptr;
82 }
83
84 inline pointer allocate(size_type n, typename std::allocator<T>::const_pointer = 0)
85 {
86 if(auto& vec = M_free_map_[n]; !vec.empty()){
87
88 void* reuse_address = vec.back();
89 std::cout << "reuse free address: " << reuse_address << '\n';
90 vec.pop_back();
91 return reinterpret_cast<pointer>(reuse_address);
92
93 } else if(M_count_ + n < MAX_OBJECTS){
94
95 void* pblock = static_cast<void*>(Mp_pool_on_heap_ + M_block_size_ * M_count_);
96 M_size_map_[pblock] = n;
97 M_count_ += n;
98 std::cout << "allocate unused address: " << pblock << '\n';
99 return reinterpret_cast<pointer>(::new (pblock) char[n * M_block_size_]);
100
101 }
102 return nullptr;
103 }
104
105 void deallocate(pointer p,size_type) {
106 size_type size_to_push = M_size_map_[(void*)p];
107 M_free_map_[size_to_push].push_back((void*)p);
108 std::cout << "deallocate: " << p << '\n';
109 }
110
111 template<typename U, typename... Args>
112 void construct(U* p, Args&&... args) {
113 std::cout << "construct: " << p << '\n';
114 ::new ((void*)p) U(std::forward<Args>(args)...);
115 }
116
117 template<typename U>
118 void destroy(U* p) {
119 p->~U();
120 }
121
122 };
123
124 struct Foo {
125 int a,b,c,d,e;
126 };
127
128 int main()
129 {
130 MapAllocator<std::pair<const Foo,int>> map_allocator_foo;
131 MapAllocator<std::pair<const Foo,Foo>> map_allocator_foo_foo;
132 typedef MapAllocator<std::pair<const int,int>> map_allocator;
133 map_allocator alloc;
134
135 std::less<int> comp;
136 std::map<int,int,std::less<int>,map_allocator> m(comp,alloc);
137 m.insert({1,2});
138 m.insert({3,4});
139 m.insert({5,6});
140 m.insert({7,8});
141 auto y = m.find(5);
142 m.erase(y);
143 m.insert({9,10});
144 std::cout << "Map Values: ";
145 for(auto const& x : m)
146 std::cout << x.first << " ";
147 std::cout << '\n';
148
149 return 0;
150 }
ビルドと実行結果.
$ g++ main.cpp -std=c++17 -g $ ./a.out M_block_size_ = 56 M_block_size_ = 72 M_block_size_ = 40 allocate unused address: 0x560d49e110d0 construct: 0x560d49e110f0 allocate unused address: 0x560d49e110f8 construct: 0x560d49e11118 allocate unused address: 0x560d49e11120 construct: 0x560d49e11140 allocate unused address: 0x560d49e11148 construct: 0x560d49e11168 deallocate: 0x560d49e11120 reuse free address: 0x560d49e11120 construct: 0x560d49e11140 Map Values: 1 3 7 9 deallocate: 0x560d49e11120 deallocate: 0x560d49e11148 deallocate: 0x560d49e110f8 deallocate: 0x560d49e110d0
まず std::map に限定するためにコンパイル時アサート文を追加しておきます。
11 template<typename T>
12 struct is_pair : std::false_type {};
13
14 template<typename T, typename U>
15 struct is_pair<std::pair<const T,U>> {
16 static constexpr bool value = true;
17 };
18
19 template<typename T, typename U>
20 struct is_pair<std::_Rb_tree_node<std::pair<const T,U>>> {
21 static constexpr bool value = true;
22 };
// 中略
36 static_assert(is_pair<T>::value);これによってテンプレートパラメーター T が std::pair または std::_Rb_tree_node<std::pair<const T,U>> である場合にのみコンパイルするよう制約します。
std::map のデータ構造のベースとなる型が std::pair である事を利用したチェックです。
次にアロケーターのブロックサイズを決定します。
50 inline MapAllocator() noexcept :
51 M_count_(0),
52 M_block_size_(sizeof(T)+sizeof(std::_Rb_tree_node_base))
53 {
54 Mp_pool_on_heap_ = static_cast<char*>(std::malloc(MAX_OBJECTS*M_block_size_));
55 std::cout << "M_block_size_ = " << M_block_size_ << '\n';
56 }テンプレートパラメーター T のバイトサイズに加えて赤黒木に必要なデータサイズ sizeof(std::_Rb_tree_node_base) を追加したサイズをブロックサイズとします。
この箇所については以下の行でチェックできます。
130 MapAllocator<std::pair<const Foo,int>> map_allocator_foo; 131 MapAllocator<std::pair<const Foo,Foo>> map_allocator_foo_foo; 132 typedef MapAllocator<std::pair<const int,int>> map_allocator; 133 map_allocator alloc;
インスタンスを作るとコンストラクターがコールされて、コンストラクターからブロックサイズを出力します。
55 std::cout << "M_block_size_ = " << M_block_size_ << '\n';
出力結果は以下のようになります。
M_block_size_ = 56 M_block_size_ = 72 M_block_size_ = 40
std::pair<const Foo,int> は 56 バイト、 std::pair<const Foo,Foo> は 72 バイト、 std::pair<const int,int> が 40 バイトになります。
std::pair<const int,int> が 40 バイトになるのは int の 4 バイトが 2 個と sizeof(std::_Rb_tree_node_base) のサイズ 32 バイトを足したからです。
STL コンテナはバイトサイズが十分でなければ、正常に動作しないので、この計算が正しければ一応は動いてくれます。
他の箇所は何の変哲もないアロケーターなので説明は割愛しますが、実のところ、この計算も不要ではあります。
前の項目で確認したのを思い出してください。
割り当て型が std::pair<const int,int> の時に allocate() 関数内では sizeof(T) が 40 バイトになりました。
てことは allocate() 関数で動的に割り当てサイズを算出するなら、計算する手間が省けるかもしれません。
次の項目ではこのアイデアを試してみたいと思います。
Copyright 2018-2019, by Masaki Komatsu