中に入れる型を決めないままコンテナだけを決める

背景

これはある実験機器から出てくる特殊なフォーマットの実験データを読むために作ったライブラリのために考えたやり方である。

そのデータは画像データとヘッダ情報から出来ており、基本的にはそれらのペアを返すことになる。だが数百KB〜数百MB程度と大きさが色々なので、メモリアロケーションの戦略を色々と変えたいことがあるだろうと考えた。

というわけで、read関数にオプションtemplate引数としてコンテナ+アロケータの種類をカスタマイズできるものを渡したい。だが、画像データとして生データを取得するか、ヘッダ情報から単位換算などを済ませたデータを倍精度・単精度浮動小数点数として読み込むかは変更できるようにもしたかった。すると、データクラスはそもそもデータ型が不定ということになる。その状況でどのようにしたらコンテナの種類だけ変えられるだろうか?

template<typename T, typename ???>
class ImageData
{
    using value_type = T;
    using container_type = ???;
    // 第二template引数で`container_type`を
    // `std::vector<T>`や`std::deque<T>`に変えられるようにしたいが……
};

解決策

みなさんはstd::allocatorのメンバ構造体rebindをご存知だろうか。細かいことを言うとC++17でdeprecatedなので知らなくても良いのだが、その場合はstd::allocator_traits::rebind_allocを見ていただきたい。これは異なる型を同じ戦略でアロケートするクラスを取り出すための型である。

例えば、連結リストは実際にはノードをアロケートする必要があるが、std::listではユーザーにノードの実装は見せていない。見せる必要もない。だが、アロケートの戦略は変えられるようになっている。すなわち、第二引数にAllocatorを取る。

namespace std
{
template<typename T, typename Alloc = std::allocator<T>>
class list;
}

アロケータには本当はstd::allocator<_Node<T>>などを渡したいわけだが、このノード型がどうなっているかはユーザーには不明である。なのでどうあれユーザーはstd::allocator<T>を渡すことになる。すると、std::listはどうにかして、ユーザー指定のアロケート戦略を尊重しつつ、_Node型をアロケートしなければならない。このために、allocatorrebindメンバを持っており、異なる型を同じ戦略でアロケートするクラスを取り出せるようになっているのだ。

何言ってんだお前という人のためにサンプルを載せておく。

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

allocatorの中に構造体が定義されており、その構造体がtemplateになっている。これによって、typedef allocator<T>::template rebind<U>::other allocator_UのようにしてUのためのアロケータを取り出せるのだ。

この発想が転用できる。

まず、以下のようなクラスを用意しておく。

struct vec
{
    template<typename T>
    struct rebind
    {
        typedef std::vector<T, std::allocator<T>> other;
    };
};

struct deq
{
    template<typename T>
    struct rebind
    {
        typedef std::deque<T, std::allocator<T>> other;
    };
};

これが渡されてくる前提で、最初のクラスで以下のようにしておく。

template<typename T, typename tagT>
class ImageData
{
    using value_type = T;
    using container_type =
        typename tagT::template rebind<value_type>::other:
};

ユーザーが呼ぶための関数を用意する。

template<typename T, typename tagT = vec>
ImageData<T, tagT> read(std::string const& file_name);

template<typename T, typename tagT>
ImageData<T, tagT> read(std::string const& file_name, tagT);

カスタムする気のないユーザーは、普通にreadを呼べば良い。カスタムする気のあるユーザーは、テンプレート引数に渡すか、第二引数としてそのクラスを渡すとよいことになる。

const auto data = read<double>("sample.dat");

const auto data1 = read<double, deq>("sample.dat");
const auto data2 = read<double>("sample.dat", deq{});

さらに、先にユーザーにこのクラスが持っておくべきものを伝えておけば、ユーザーが作ったアロケータやコンテナすら射程に入る。

struct user_alloc
{
    template<typename T>
    struct rebind
    {
        typedef std::vector<T, user::allocator<T>> other;
    };
};

struct user_container
{
    template<typename T>
    struct rebind {typedef user::container<T> other;};
};

ユーザー指定のコンテナのインターフェースがstd::vectorなどと同じなら、ライブラリの実装は買える必要はない。

違う場合も、メンバ関数呼び出しをフックできるようにしておけば最小限の変更で対応できる。

例えばsize関数呼び出しには、以下のような関数を用意し、ライブラリ内部ではそれだけを使っておけば、自分でコンテナを作るレベルのユーザーはそれをフックして対応してくれるだろう。

// size() メンバ関数があればそれを呼ぶ
template<typename T, typename std::enable_if<
    has_mem_func_size<T>::value, std::nullptr_t>::type = nullptr>
std::size_t size(const T& container);

// sizeメンバがないコンテナを使うユーザーは、オーバーロードを追加する
template<typename T>
std::size_t size(const user::container<T>& c)
{
    return c.get_size();
}

他の解決策

ところで、ImageDataクラスはtemplate template引数をとるような実装もできる。

template<typename T, template<typename> class containerT>
class ImageData
{
    using value_type = T;
    using container_type = containerT<T>;
};

template<typename T>
using deq = std::deque<T>:

const auto data = read<double, deq>("sample");

この実装はシンプルだし、template引数だけで完結するなら何も問題はない。どうせstd::vectorより凄いコンテナを作ってくる人間は限界突破しているはずなので、この程度にはひるまないだろう。

唯一なにかあるとしたら、

const auto data = read<double>("sample.dat", deq{});

のような呼び出しが出来ないことだろうか。ディスパッチ用の何かは今回エイリアスなので、実体化出来ない。まあしかし、それだけだ。