え? カスタムアロケーターの STL コンテナのサポートって難しいの?
おら、やてるよ…… (´・ω・`)
そう考えていた時も筆者にはありましたね。
ええ…
でも new や delete でメモリー管理されているヒープ領域のアロケーターで STL コンテナをサポートするのは、まあできるっちゃできます。
メモリーの管理は new や delete がやってくれるので、後はアロケーター要件( Allocator Requirement )に準拠すれば勝手に動きそうです。
まあ難しいこと考えずに、試しにそれっぽいのをコーディングしてみましょう。
main.cpp.
1 #include <climits>
2 #include <limits>
3 #include <cstdlib>
4 #include <memory>
5 #include <vector>
6 #include <list>
7 #include <map>
8 #include <iostream>
9
10 template<typename T>
11 class my_allocator
12 {
13 public:
14 typedef T value_type;
15 typedef value_type* pointer;
16 typedef const value_type* const_pointer;
17 typedef value_type& reference;
18 typedef const value_type& const_reference;
19 typedef std::size_t size_type;
20 typedef std::ptrdiff_t difference_type;
21
22 template<typename U>
23 struct rebind {
24 typedef my_allocator<U> other;
25 };
26
27 inline explicit my_allocator(){
28 std::cout << "sizeof(T) @ default constructor: " << sizeof(T) << '\n';
29 }
30
31 inline ~my_allocator(){}
32
33 inline explicit my_allocator(my_allocator const&){}
34
35 template<typename U>
36 inline explicit my_allocator(my_allocator<U> const&){
37 std::cout << "sizeof(U) @ copy constructor: " << sizeof(U) << '\n';
38 }
39
40 inline pointer allocate(size_type cnt,
41 typename std::allocator<void>::const_pointer = 0)
42 {
43 std::cout << "sizeof(T) @ allocate(): " << sizeof(T) << '\n';
44 return reinterpret_cast<pointer>(new char[cnt*sizeof(T)]);
45 }
46
47 inline void deallocate(pointer p, size_type)
48 {
49 ::operator delete(p);
50 }
51
52 inline size_type max_size() const
53 {
54 return std::numeric_limits<size_type>::max();
55 }
56 };
57
58 template<typename T>
59 using rebind_t = typename my_allocator<T>::template rebind<T>::type;
60
61 template<typename T, typename T2>
62 inline bool operator==(my_allocator<T> const&,
63 my_allocator<T2> const&)
64 {
65 return true;
66 }
67
68 template<typename T, typename OtherAllocator>
69 inline bool operator==(my_allocator<T> const&,
70 OtherAllocator const&)
71 {
72 return false;
73 }
74
75 int main()
76 {
77 using alloc = my_allocator<int>;
78 std::vector<int,alloc> vec;
79
80 vec.push_back(5);
81 vec.push_back(10);
82 vec.push_back(15);
83 vec.pop_back();
84 vec.push_back(20);
85
86 std::cout << "std::vector<int> values: ";
87 for(auto const& x : vec)
88 std::cout << x << " ";
89 std::cout << '\n';
90
91 std::list<int,alloc> lst;
92
93 lst.push_back(5);
94 lst.push_back(10);
95 lst.push_back(15);
96 lst.pop_back();
97 lst.push_back(20);
98
99 std::cout << "std::list<int> values: ";
100 for(auto const& x : lst)
101 std::cout << x << " ";
102 std::cout << '\n';
103
104 using map_alloc = my_allocator<std::pair<const int,int>>;
105 std::less<int> comp;
106 map_alloc palloc;
107 std::map<const int,int,std::less<int>,map_alloc> mp(comp,palloc);
108 mp.insert({1,2});
109 mp.insert({2,3});
110 mp.insert({4,5});
111 auto key = mp.find(4);
112 mp.erase(key);
113 mp.insert({5,6});
114
115 std::cout << "std::map<const,int> values: ";
116 for(auto const& x : mp){
117 std::cout << x.second << " ";
118 }
119 std::cout << '\n';
120
121 return 0;
122 }
ビルドと実行結果.
$ g++ main.cpp $ ./a.out sizeof(T) @ default constructor: 4 sizeof(T) @ allocate(): 4 sizeof(T) @ allocate(): 4 sizeof(T) @ allocate(): 4 std::vector<int> values: 5 10 20 sizeof(T) @ default constructor: 24 sizeof(T) @ allocate(): 24 sizeof(T) @ allocate(): 24 sizeof(T) @ allocate(): 24 sizeof(T) @ allocate(): 24 std::list<int> values: 5 10 20 sizeof(T) @ default constructor: 8 sizeof(U) @ copy constructor: 8 sizeof(T) @ allocate(): 40 sizeof(T) @ allocate(): 40 sizeof(T) @ allocate(): 40 sizeof(T) @ allocate(): 40 std::map<const,int> values: 2 3 6
ううん… 動いておる… (´・ω・`)
それで STL コンテナの動作は分かるのですが、このソースコードでは allocate() 関数が割り当てるバイトサイズを出力するようにしています。
何の変哲もない実装なんですけど。。。
40 inline pointer allocate(size_type cnt,
41 typename std::allocator<void>::const_pointer = 0)
42 {
43 std::cout << "sizeof(T) @ allocate(): " << sizeof(T) << '\n';
44 return reinterpret_cast<pointer>(new char[cnt*sizeof(T)]);
45 }
46
47 inline void deallocate(pointer p, size_type)
48 {
49 ::operator delete(p);
50 }まあ new char[] と delete[] を使ってるので、メモリーの管理は malloc とかが裏でよろしくやってくれてます。
そりゃ動きますよね。
それと ::operator new と ::operator delete でも対応できるので試してみてくださいね。
ということで allocate() をコールするどうなるのか、テストをしたいと思います。
まずは std::vector をチェックします。
77 using alloc = my_allocator<int>; 78 std::vector<int,alloc> vec; 79 80 vec.push_back(5); 81 vec.push_back(10); 82 vec.push_back(15); 83 vec.pop_back(); 84 vec.push_back(20); 85 86 std::cout << "std::vector<int> values: "; 87 for(auto const& x : vec) 88 std::cout << x << " "; 89 std::cout << '\n';
この例では 5,10,15 をプッシュして 15 をポップ、最後に 20 をプッシュしているので 5,10,20 の要素があることになります。
出力と照合してみましょう。
sizeof(T) @ default constructor: 4 sizeof(T) @ allocate(): 4 sizeof(T) @ allocate(): 4 sizeof(T) @ allocate(): 4 std::vector<int> values: 5 10 20
パット見た限りでは誤ったところは無いですね。
ちなみにデフォルトコンストラクターで出力させた sizeof(T) の結果は 4 でした。
次は std::list です。
91 std::list<int,alloc> lst; 92 93 lst.push_back(5); 94 lst.push_back(10); 95 lst.push_back(15); 96 lst.pop_back(); 97 lst.push_back(20); 98 99 std::cout << "std::list<int> values: "; 100 for(auto const& x : lst) 101 std::cout << x << " "; 102 std::cout << '\n';
やってることは std::vector のケースと全く同じです。
では該当する出力です。
sizeof(T) @ default constructor: 24 sizeof(T) @ allocate(): 24 sizeof(T) @ allocate(): 24 sizeof(T) @ allocate(): 24 sizeof(T) @ allocate(): 24 std::list<int> values: 5 10 20
コンストラクターと allocate() 関数内の sizeof(T) 関数は 24 に評価されています。
この 24 が何を示すかは後に説明しますが、双方向連結リストにはポインターが 2 個とデータが必要なので、それっぽい値ですね。
最後は std::map についてです。
104 using map_alloc = my_allocator<std::pair<const int,int>>;
105 std::less<int> comp;
106 map_alloc palloc;
107 std::map<const int,int,std::less<int>,map_alloc> mp(comp,palloc);
108 mp.insert({1,2});
109 mp.insert({2,3});
110 mp.insert({4,5});
111 auto key = mp.find(4);
112 mp.erase(key);
113 mp.insert({5,6});
114
115 std::cout << "std::map<const,int> values: ";
116 for(auto const& x : mp){
117 std::cout << x.second << " ";
118 }
119 std::cout << '\n';std::map については (1,2), (2,4), (4,5) のペアをプッシュして (4,5) をポップします。
次に (5,6) のペアをプッシュしています。
では該当する出力もみてみましょう。
sizeof(T) @ default constructor: 8 sizeof(U) @ copy constructor: 8 sizeof(T) @ allocate(): 40 sizeof(T) @ allocate(): 40 sizeof(T) @ allocate(): 40 sizeof(T) @ allocate(): 40 std::map<const,int> values: 2 3 6
これによると allocate() 関数を呼び出すときには sizeof(T) は 40 バイトになり、コンストラクターを呼び出すときには 8 バイトになっています。
なぜ 40 バイトなのだろう?
謎の挙動です…
(´・ω・`)
これはアルゴリズムを実装するためのデータらしいですが、筆者も詳しい実装は知らないですね…
Copyright 2018-2019, by Masaki Komatsu