第85章 アロケーターに必要な最低限の関数・オーバーロード・テンプレート

筆者はアロケーターの設計思想を学んで心を揺り動かされている人を見たことがないです。

その理由は要件がユルいことかなと思います。

オブジェクト指向言語ではインターフェースを継承・オーバーライドすることが中心的な要件であると考える人は多いからです。

でも現実のアロケーターはというとドキュメントを見ながら、あーでもない、こーでもないと取り捨てながら実装していきます。

逆に言えば一旦APIを理解したら無駄なソースコードを書かずシンプルにできる利点もありますが、小さいミドルウェアを書く時に必要なぐらいは自分でリサーチしないといけなくなります。

つまり最低限の実装はあっても、どこを目標とするかによって最低限の定義が大幅に変わってしまいます。

要はですね… ナマケモノにはきつい仕組みになってるってことですね。

では前置きが長くなったので要件を説明していきたいと思います。

まずはインターフェースです。

#include <memory>

template<typename T, typename Allocator = std::allocator<T>>
class A {
public:
  typedef typename std::allocator_traits<Allocator>::value_type value_type;
  typedef typename std::allocator_traits<Allocator>::pointer pointer;
  typedef typename std::allocator_traits<Allocator>::const_pointer const_pointer;
  typedef typename Allocator::reference reference;
  typedef typename Allocator::const_reference const_reference;
  typedef typename std::allocator_traits<Allocator>::size_type size_type;
  typedef typename std::allocator_traits<Allocator>::difference_type difference_type;
  typedef typename std::allocator_traits<Allocator>::const_void_pointer const_void_pointer;
  typedef Allocator allocator_type;

  template<typename U>
  struct rebind {
    typedef A<U> other;
  };

  pointer allocate(size_type n, const_void_pointer cvp = const_void_pointer());
  void deallocate(pointer p, size_type n);
  size_type max_size();

  template<typename U, typename... Args>
  void construct(U* p, Args&&... args);

  template<typename U>
  void destroy(U* p);

  pointer address(reference r) const noexcept;
  const_pointer address(const_reference cr) const noexcept;

private:
  allocator_type M_allocator_;
};

template<typename T1, std::size_t N, typename T2>
bool operator==(const A<T1>& lhs, const A<T2>& rhs);

template<typename T1, std::size_t N, typename T2>
bool operator!=(const A<T1>& lhs, const A<T2>& rhs);

 このインターフェースのうち必須と言えそうなものは以下の 3 つです。

 コンストラクターはもちろん必要ですが、筆者の経験から判断して、ひっかかるのはこの 3 つだと思います。

 とはいえ状況によりけりかなあとも思っちゃうんですよね…

 (・∀・)

 では一つづつ説明してきましょうかね。

 まずは定義からです。

A
T 型のオブジェクト向けのアロケーター型
B
U 型のオブジェクト向けのアロケーター型
a
A 型のオブジェクト
b
B 型のオブジェクト

 定義されたシンボルを使うと、アロケーターに最低限欲しいコンストラクターやオーバーロードを定義できます。

a1 == a2
二項等値演算子のオーバーロード
a1 != a2
二項等値演算子のオーバーロード
A a(b)
アロケーターとは異なる型を引数とするコピーコンストラクター
A a(a1)
アロケーターAと同じ型を引数とするコピーコンストラクター

 ここいらはアロケーターでなくとも実装するので驚きはないかと思います。

 それで良いですよね?

 そして読者が自分の頭を使って実装しないといけない関数やクラスは以下の5つです。

void * allocate(std::size_t n)
新規にメモリー領域を割り当てる際に使います
void deallocate(T* p, std::size_t n)
割り当てた領域を解放します
std::size_t max_size()
サイズの最大値を返します
T * construct(U* p, Args… args)
既に割当てられたメモリー領域のアドレスにおいて T* を作ります
void destroy(T* p)
T型のオブジェクトを破壊し p を解放します

 最後にオマジナイです。

  template<typename U>
  struct rebind {
    typedef A<U> other;
  };

この rebind クラステンプレートは以下のようにしてアクセスできます。

A::template rebind<U>::other
全ての U に対して B::template rebind<T>::other は A となります

other はクラステンプレート内のクラステンプレートにアクセスするための表現です。

これは std::allocator_traits クラステンプレートの rebind_alloc クラステンプレートを使うと用途がわかるかと思います。

std::allocator_traits<A>::rebind_alloc<U> my_instance;

 これによって T 型でない他のアロケーター型を、クラステンプレート A から導きだすことができます。

 この rebind_alloc クラステンプレートを使うための前提条件が以下の表現が使えるための、オマジナイです。

A::rebind<U>::other my_instance;

アロケーターをリバインドするためのクラステンプレートというよりは std::allocator_traits に合わせて作られた要件だと筆者は考えています。

つまり T 型にバインドされたものを U 型にバインドし直して使うためという理屈なんですが C++ 標準ライブラリーとして便利なために付け加えられた要件という気がします。

まあ… 筆者程度の知識だとアロケーター要件を完コピして、エッヘンしてる程度の些末な経験からひねり出す感じです。

 全部理解していないけど使用する段階になってやっと追い詰められて絞り出すのが常ですね…

 (´・ω・`)

 クラステンプレートをそのまま使わずにリバインドから型を using や typedef で定義することもありますんで、それは後半の章で簡単な例ぐらいは説明しときます。

 でも元はと言えば STL 側に合わせるためのクラステンプレートだと思いっているので、何も考えずにつける感じで良いかと思います。

 ちなみに内部にアロケーターのインスタンスがありますね。

template<typename T, typename Allocator = std::allocator<T>>
class A {
  typedef Allocator allocator_type;
private:
  allocator_type M_allocator_;
};

 これは必要に応じて Allocator を変更するために使います。

 std::allocator を使っても結局同じになっちゃいますからね。

 本当に重要なのは std::allocator を自前で作って、必要なタイミングで切り替えするような機能だと思います。

Copyright 2018-2019, by Masaki Komatsu