C++

C++11で作るTypeList(型情報コンテナ)とfor_each

投稿日:2018年6月30日 更新日:

経緯

仕事で、他人が書いたコードを見ているとき、こんなコードを発見してバツが悪くなった。

int main() {
  Foo &foo = Foo::GetInstance();
  foo.Initialize();

  Bar &bar = Bar::GetInstance();
  bar.Initialize();

  Hoge &hoge = Hoge::GetInstance();
  hoge.Initialize();

  Huga &huga = Huga::GetInstance();
  huga.Initialize();

開発途中で必要なモジュールを追加で実装していった結果であって、しょうがないことだが、リファクタリングは行わなければならない。

上記コードで問題なのは、モジュール数が増えたとき、実装の変更量が多いことだ。
そこで、各クラスをの型を格納するコンテナを用意し、コンテナ内のすべての型を実体化したくなる。

using ModuleList = TypeList<Foo, Bar, Hoge, Huga>  // 型情報を持つコンテナ。ヘッダに出しておくとなお良い

template <typename T>
void Initialize() {  // シングルトンな型を実体化する関数
  T &t = T::GetInstance();
  t.Initalize();
}

int main() {
  ForEach<ModuleList>::Apply(Initialize);  // 例えばこんな感じ
}

こうすれば、初期化したいモジュールを新規追加したいときに、型情報を持つコンテナを追加するだけですむわけだ。

要件と仕様

要件は以下。

  • 特定の処理をすべてのクラスに施し、インスタンス化するための手段を実装する

仕様は以下。

  • 型を保持するリストを実装する
  • 上記リストに対し、一様に関数を適用できるようなfor_each相当のものを実装する

設計と実装

型情報を持つコンテナなんて便利なものないかなぁと思い探してみると、なんとC++03時代にすでにライブラリ化されていることがわかった。

LokiのTypeList

C++のテンプレート機能を駆使ししたライブラリの一つにLokiが存在する。
Lokiのメインとなる機能は、上記のように型情報を静的に保持することができるコンテナであるTypeListだ。

LokiはC++03時代に書かれたコードだ。当時はC++11のような可変長引数テンプレートが使用できなかったために、以下のようにTypeListで保持できる型の個数分のマクロが切られていた。

#define TYPELIST_1(T1) Typelist<T1, NullType>
#define TYPELIST_2(T1, T2) Typelist<T1, TYPELIST_1(T2)>
#define TYPELIST_3(T1, T2, T3) Typelist<T1, TYPELIST_2(T2, T3)>
// ...
#define TYPELIST_50(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, \
        T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, \
        T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, \
        T31, T32, T33, T34, T35, T36, T37, T38, T39, T40, \
        T41, T42, T43, T44, T45, T46, T47, T48, T49, T50) \
    Typelist<T1, TYPELIST_49(T2, T3, T4, T5, T6, T7, T8, T9, T10, \
        T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, \
        T21, T22, T23, T24, T25, T26, T27, T28, T29, T30, \
        T31, T32, T33, T34, T35, T36, T37, T38, T39, T40, \
        T41, T42, T43, T44, T45, T46, T47, T48, T49, T50)>

しかしときはすでに2018年、C++11がすでにメジャーになっている今、LokiのTypeListはもっと簡単に書き換えられるはず。
ということで、C++11の記法を使用して、TypeListと、それに対するfor_eachを実装してみる。

C++11時代のTypeList

C++11では可変長引数テンプレートが言語機能として実装されている。
これは、任意の数のテンプレート引数を取る関数・クラステンプレートが作成可能になるものだ。
これを使わない手はない。

可変長引数テンプレートによって、LokiのTypeListのように引数の数だけマクロを作成することがなくなる。

可変長引数テンプレートを使用したTypeListは以下のように書ける。

struct NullType {}; 

template <typename T, typename U>
struct TypeList {
  using Head = T;
  using Tail = U;
};


template <typename...> struct MakeTypeList;
template <>
struct MakeTypeList<> {
  using Result = NullType;
};
template <typename Head, typename... Types>
struct MakeTypeList<Head, Types...> {
  using Result = TypeList<Head, typename MakeTypeList<Types...>::Result>;
};

TypeListはテンプレートの再帰を利用している。HeadとTailの2要素をもち、Tail内にさらにTypeListを連結させることで、2つ以上の型をもつリストが展開できるというわけだ。
15行目付近で可変長引数テンプレートを用いて、Tail以降に連結されるTypeListが任意の数のテンプレートパラメタを保持できるように定義している。これのお陰で、Tail以降が複数の方を保持していても、マクロを利用することなくリストが展開される。

TypeListを用いると、例えば以下のようにコードが実体化される。

MakeTypeList<int, char, st_Foo>::Result
  = TypeList<Head = int, Tail = TypeList<Head = char, Tail = TypeList<Head = st_Foo, Tail = NullType>>>

TypeListが次々と再帰的に展開されるわけだ。

TypeListへのアクセッサ(リストのサイズ・添字アクセス・型からの添字抽出)

ついでに各種アクセッサも実装しておこう。

// リストのサイズを得る
template <typename> struct Length;
template <>
struct Length<NullType> {
  enum { value = 0 };
};
template <typename Head, typename Tail>
struct Length<TypeList<Head, Tail>> {
  enum { value = Length<Tail>::value + 1 };
};

// リストの添字を指定して型を返す
template <typename, typename> struct IndexOf;
template <typename T>
struct IndexOf<NullType, T> {
  enum { value = -1 };
};
template <typename T, typename Tail>
struct IndexOf<TypeList<T, Tail>, T> {
  enum { value = 0 };
};
template <typename Head, typename Tail, typename T>
struct IndexOf<TypeList<Head, Tail>, T> {
  using Result = IndexOf<Tail, T>;
  enum { value = Result::value == -1 ? -1 : Result::value + 1 };
};

// リストにある型を指定して添字を返す
template <typename T, uint32_t U, typename DefaultType = NullType>
struct At {
  using Result = NullType;
};
template <typename Head, typename Tail, typename DefaultType>
struct At<TypeList<Head, Tail>, 0, DefaultType> {
  using Result = Head;
};
template <typename Head, typename Tail, uint32_t index, typename DefaultType>
struct At<TypeList<Head, Tail>, index, DefaultType> {
  using Result = typename At<Tail, index - 1, DefaultType>::Result;
};

使い方は最後の項(テスト)内にあるサンプルコードを見てほしい

TypeListに任意の関数を適用するfor_each

さて、あとはfor_eachだ。
for_eachでは任意の関数を型リスト内の型全てに適用する。そのため、for_eachは関数テンプレートな関数オブジェクトかファンクタを保持できるようにしなければならない。

C++11で実装するのだから、std::functionを使用して書きたいところだが、std::functionで関数テンプレートを渡すためには、予め型を指定して実体化する必要がある。(参考:下記ページ参照)

今回はTypeListにどのような型が入るかがわからないため、事前に型を実体化することはできない。
よって、std::functionを使用することは諦め、ファンクタを使用する方法を実装する。

template <typename> struct ForEach;
template <typename Head, typename Tail>
struct ForEach <TypeList<Head, Tail>> {
  template <typename Functor>
  static void Apply(Functor &f) {
    using Type = Head;
    f.template operator()<Type>();
    ForEach<Tail>::Apply(f);
  }
};
template <>
struct ForEach<NullType> {
  template <typename Functor>
  static void Apply(Functor &f) {}
};

ポイントは7行目である。
テンプレートパラメータFunctorからメンバ関数テンプレートを呼び出すためには、f.templateのように、呼び出す関数がテンプレートを使用することを明示的に書く必要がある(これがないとコンパイルが通らない)

ファンクタは下記のように書く。

class TestFunctor {
 public:
  template <typename T>
  void operator()() {
    std::cout << "functor type:" << typeid(T).name() << std::endl;
  }
};

operator() をメンバ関数テンプレートにすることで、任意の型に対しoperator()を実行できるようになる。

テスト

環境

テスト環境は以下である。

$ uname -a
Linux (略) 4.4.88-mainline-rev1 #1 SMP Wed Sep 13 23:49:03 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609

テストコードは以下である。

#include <iostream>
#include <cstdint>
#include <vector>
#include <memory>
#include <string>
#include <typeinfo>

#include "MetaTypeList.h"

typedef struct st_Foo {
} st_Foo;

class TestFunctor {
 public:
  template <typename T>
  void operator()() {
    std::cout << "functor type:" << typeid(T).name() << std::endl;
  }
};
  

int main() {
  using IntTypes = typename MakeTypeList<char, bool, int>::Result;

  std::cout << "Length<> : " << Length<IntTypes>::value << '\n';
  std::cout << "IndexOf<char> : " << IndexOf<IntTypes, char>::value << '\n';
  std::cout << "IndexOf<bool> : " << IndexOf<IntTypes, bool>::value << '\n';
  std::cout << "IndexOf<int> : " << IndexOf<IntTypes, int>::value << '\n';
  std::cout << "IndexOf<void> : " << IndexOf<IntTypes, void>::value << '\n';

  At<IntTypes, 0>::Result c = '1';
  At<IntTypes, 1>::Result b = true;
  At<IntTypes, 2>::Result i = 5;

  std::cout << "At<0> c: " << c << '\n';
  std::cout << "At<1> b: " << b << '\n';
  std::cout << "At<2> i: " << i << '\n';

  using IntTypes2 = typename MakeTypeList<int, bool, st_Foo>::Result;
  TestFunctor f;
  ForEach<IntTypes2>::Apply(f);
}

実行結果は以下となった。

$ g++ main.cpp -std=c++11 ; ./a.out 
Length<> : 3
IndexOf<char> : 0
IndexOf<bool> : 1
IndexOf<int> : 2
IndexOf<void> : -1
At<0> c: 1
At<1> b: 1
At<2> i: 5
functor type:i
functor type:b
functor type:6st_Foo

型リストの作成・各種アクセッサの動作、for_eachの動作が確認できた。

最後に

作成したTypeList関連定義ファイルMetaTypeList.hを以下のgistにアップロードした。

-C++
-,

執筆者:


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

関連記事

cmake, googletestを利用したC++開発環境の構築テスト

自学用メモ。 業務で使用しているcmakeの勉強がてら、業務に近いソースツリーを作成した。 googletestを用いた単体テストも合わせて書けるような構成になっている。 テンプレートとして、今後のC …

ltraceを用いてstd::vector::push_backの動作を見える化する

仕事で使用したltraceの使い方を、メモがてら残しておきます。 目次1 概要1.1 環境2 設計・実装3 ltraceを用いたdynamic libraryのトレース4 終わりに5 メモ5.0.1 …

pthread_cancelすると何が行われるのか

仕事でのメモ。 目次1 pthread_cancel2 pthread_cancelで何が行われるのか2.1 シグナル送出2.2 注意2.3 ソースコードを読む2.4 pthread_cancel時の …

C++メタプログラミングでmemcpyを少し安全にする

目次1 背景2 環境3 要件と仕様4 設計・実装5 テスト6 まとめ6.0.1 関連 背景 筆者はC++11を使用して開発を行っています。 開発していると、危険なlibcの関数をラップしてUtilit …

no image

cmakeでのDEBUG/RELEASEモード別・サブディレクトリ別のコンパイルオプションの分け方

仕事で使うので備忘録として。 対象となるソースコードツリーは、拙作のcppのプロジェクトのテンプレートで行う shinjikirino/cpp_project_templatehttps://gith …