第98章 STL コンテナのサポート ( new / delete )

え? カスタムアロケーターの 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