筆者はアロケーターの設計思想を学んで心を揺り動かされている人を見たことがないです。
その理由は要件がユルいことかなと思います。
オブジェクト指向言語ではインターフェースを継承・オーバーライドすることが中心的な要件であると考える人は多いからです。
でも現実のアロケーターはというとドキュメントを見ながら、あーでもない、こーでもないと取り捨てながら実装していきます。
逆に言えば一旦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 つだと思います。
とはいえ状況によりけりかなあとも思っちゃうんですよね…
(・∀・)
では一つづつ説明してきましょうかね。
まずは定義からです。
定義されたシンボルを使うと、アロケーターに最低限欲しいコンストラクターやオーバーロードを定義できます。
ここいらはアロケーターでなくとも実装するので驚きはないかと思います。
それで良いですよね?
そして読者が自分の頭を使って実装しないといけない関数やクラスは以下の5つです。
最後にオマジナイです。
template<typename U> struct rebind { typedef A<U> other; };
この rebind クラステンプレートは以下のようにしてアクセスできます。
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