読者です 読者をやめる 読者になる 読者になる

C++ 可変長引数テンプレートのお話

 前回のソースコードの中で1点だけ、C++11らしい機能の紹介を。
 C++11の機能という点ではgl/InputConstants.h内でenum classを使ってはいますが、基本的なことしかしていませんので省略します。

●可変長引数テンプレート
 glut/Window.hに1ヵ所だけ、テンプレートメンバ関数が含まれていることにお気づきでしょうか?

glut/Window.h

/**
 * コールバックを各々のウィンドウの指定メソッドに割り当てる処理。
 */
template <class ... TParams>
static void execute(void (Window::*method)(gl::Context &, TParams ...), TParams ... parameters) ;

 実装部分は次の通りです:

glut/Window.cpp

template <class ... TParams>
void glut::Window::execute(void (Window::*method)(gl::Context &, TParams ...), TParams ... parameters)
{
  // 現在のウィンドウIDからウィンドウを取得
  auto it = m_id_map.find(glutGetWindow()) ;
  if (it == m_id_map.end()) return ;

  // ウィンドウに対して、指定したメソッドを呼び出す
  (it->second->*method)(it->second->m_context, parameters ...) ;
}

 本来であれば、テンプレート関数の定義はヘッダ内で行うものですが(テンプレート関数の定義は翻訳単位ごとに必要になるため)、どうせglut/Window.cpp内部でしか使わないのでglut/Window.cpp内に入れてあります。

 この関数のテンプレート宣言には、と、classとTParamsの間に ... が入っています。
 これがC++11で2番目に強力(当社調べ)な新機能で、可変長引数テンプレートと呼ばれる機能です(なお、1番はラムダで、3番はムーブセマンティクスだと思っていますが異論は認めます)。

 可変長引数テンプレートはtemplate<型 ... 値>の形で宣言し、unpack演算子...を使う事で、式をテンプレートで渡した値の数だけ,区切りにして渡してくれます。
 例えばこんな感じです:

template <class ... T>
std::tuple<const T*...> hoge(const T& ... x) { return std::make_tuple(&x + 1 ...) ; }

 上記のコードでは、unpack演算子を3回使っていますが、正常にコンパイルできます。
 関数の場合、テンプレート引数が関数の引数から求まる場合はテンプレート引数の指定を省略できるので、次のようなコードでは、各hogeはコメントのような処理に展開されます。

int x[] = {0, 1} ;
double y[] = {0.0, 5.0} ;
auto tuple0 = hoge() ; // std::tuple<> hoge<>() { return std::make_tuple() ; }
auto tuple2 = hoge(x[0], y[0]) ; // std::tuple<const int*, const double*> hoge<int, double>(const int& x0, const double& x1) { return std::make_tuple(&x0 + 1, &x1 + 1) ; }

assert(1 == *std::get<0>(tuple2)) ;
assert(5.0 == *std::get<1>(tuple2)) ;

 単純にTをカンマ区切りにするわけではなく、Tを含む式を丸ごとコピーしてくれているのがわかります。

glut/Window.cppでの使用
 さて、ここでもう一度glut/Window.cppのほうに戻ります。

glut/Window.h

template <class ... TParams>
static void execute(void (Window::*method)(gl::Context &, TParams ...), TParams ... parameters) ;

 execute()の第一引数は、gl::Contextへの参照を第一引数とし、第二引数以降はテンプレートで指定した通りの型を持つ、Windowクラスのメンバ関数です。
 また、execute()の第二引数以降は、テンプレートで指定した型の値です。

 ではexecute()の中身をもう一度見ていきます。

glut/Window.cpp

template <class ... TParams>
void glut::Window::execute(void (Window::*method)(gl::Context &, TParams ...), TParams ... parameters)
{
  // 現在のウィンドウIDからウィンドウを取得
  auto it = m_id_map.find(glutGetWindow()) ;
  if (it == m_id_map.end()) return ;

  // ウィンドウに対して、指定したメソッドを呼び出す
  (it->second->*method)(it->second->m_context, parameters ...) ;
}

 前半部は、ウィンドウIDからglut::Windowへのポインタを取得しているだけのコードです。
 後半部では、見つかったウィンドウに対してメンバ関数を呼びだしています(C++無数の鬼門の1つと言われるアレです)。この際、execute()に渡した引数をそのまま引数に渡してやっています。
 その結果、glutに登録したstaticなコールバック関数内からウィンドウIDに応じたオブジェクトのメンバー関数を呼び出すための処理が、次のように明瞭簡潔に書けることになります。

glut/Window.cpp

void glut::Window::displayFunc()
{
  execute(&Window::display) ;
}

void glut::Window::reshapeFunc(int width, int height)
{
  execute(&Window::reshape, width, height) ;
}

●余談
 Web上を見ても日本語の情報がほとんど見つかりませんでしたが、実は、可変長引数テンプレートはclass/typenameだけでなく、整数型にも使えるようです。

// 指定した値を(値の数)倍し、それらの合計を求める
template <int ... N>
int hoge()
{
  int s = 0 ;
  for (int x : std::initializer_list<int>({N * sizeof...(N) ...})) {
    s += x ;
  }
  return s ;
}

 ここで、for (int x : {N * sizeof...(N) ...})とするとテンプレート引数の数が0だった場合に「型が不明」と言われるので、明示的にinitializer_listでくるんでいます。

 まあ、整数型を可変長にしたいケースが世の中にどれだけあるかは疑問ですが、一応参考までに。