(C++0x)可変長テンプレート引数は再帰でおいしくいただきましょう

C++0xで導入される可変長テンプレート引数,便利ですね.
僕は静的なのが大好きなので.

可変長テンプレート引数の何がおいしいかというと,例えば現在のboost::tupleはT0からT9までをデフォルトテンプレート引数と組み合わせて実装してるので要素数は10以下に限定されますが,C++0xに用意されるstd::tupleは可変長テンプレート引数で実装されるので,原理的には,無限の要素数が実現できます.
ここで本質的に重要なのは,「無限の要素数」に対応できることではなく,「任意の要素数nにおいて一般的に記述できる」ことである点に注意.

また,可変長テンプレート引数を用いて,可変長引数の関数を書けます.
これは,いわば「型安全printf」を書けることを意味します.

話がちょっと逸れましたが,じゃあその可変長テンプレート引数は実際どういう風に扱うの?という話を,関数テンプレートに関してします.
Wikipediaに書いてある範囲より僅かながら広い内容,だと思います.たぶん.

用語をはっきりさせときましょう

  • テンプレート仮引数

例えばtemplate<class T> class Hoge; におけるTが,テンプレート仮引数です.

  • テンプレート実引数

例えばtemplate<class T> class Hoge; なクラス対してHoge<int> ob; としたときのintが,テンプレート実引数です.

  • 可変長テンプレート仮引数と可変長テンプレート実引数

詳細は後述しますが,template<class… Args> void func(); なクラスにおけるArgsが可変長テンプレート仮引数,これに対してfunc<int, double, char>()などとしたときのint,double,charが可変長テンプレート実引数です.

  • 仮引数・実引数・可変長引数

「テンプレート」を付けなかったら,関数のパラメータとしての意味で言ってます.
void func(int a);な関数のaが仮引数,呼び出しfunc(100);の100が実引数…と言った感じです.

可変長テンプレート引数を受け取る関数の定義

詳細は他に譲るとして.

template<class... Args>
void func(Args... args)
{
    //...
}

//constをつけましょう.
template<class... Args>
void func(const Args&... args)
{
    //...
}

“…”演算子が2回現れてますが,テンプレート仮引数Argsの前にくる場合は”pack演算子”,後にくる場合は”unpack演算子”と呼びます.あと後述しますが実引数の後に書く場合もunpack演算子と言います.
意味はなんとなく想像できることと思います.

あとconst参照で受け取るようにするには上に書いたようにします.

で,この関数は

func<int, double, std::string>(10, 10.5, std::string("hoge"));

で呼び出せます.

関数の実引数に基づいた型推論によってテンプレート実引数も決定できるので,単に型指定を省略して,

func(10, 10.5, std::string("hoge"));

とも書けます.
ここんとこがあとでちょっと重要になります.

ところでこれを見ると,まさに(テンプレートを使わない)可変長引数の関数であるかのように使えますね.

また,可変長テンプレート引数を丸投げすることもできます.

template<class... Args>
void func()
{
    //...
}

template<class... Args>
void func2()
{
    func<int, Args..., int>();
}

//func2<double, double>()を呼ぶことは,ここではfunc<int, double, double, int>()を呼ぶことと同じになる.

このようにunpack演算子を用いて,可変長テンプレート実引数を,何らかの可変長テンプレート引数を受け取る関数にまるごと投げることができます.

逆のこともできます.

template<class T, class... Args>
void func()
{
    //...
}

template<class... Args>
void func2()
{
    func<Args...>();
}

//ここでfunc2<int, double, float>()としたとき,funcにおいてT=int,Args={double, float}となる.

この辺の話を,記事のメインでがっつり使います.

で,この記事で話すのは,「じゃあfuncの中でArgsをどう扱うよ?」という話です.

本題1:関数に(テンプレート実引数で指定した型に対応する)可変長引数がある場合

例えば

 func<char, int>('a', 10);
func<int, double, std::string>(10, 10.5, std::string("hoge"));

という形で呼び出される関数について考えます.
つまり,テンプレート実引数に対応する引数を受け取る関数funcについて,そのfuncの中で実際に指定された型や引数に与えられた値にどうやってアクセスするか,という話です.

可変長テンプレート実引数の各要素へ直接アクセスすることはできない

例えばC#の普通のメソッドでの可変長引数の扱い方は非常に分かりやすいですよね.
あれはそもそも型テンプレートじゃないので比べる時点でいけないのですがそれを承知の上で言うと,あんな雰囲気で各テンプレート実引数へ順次アクセスする方法はありません.

何が言いたいかというと,可変長テンプレート引数を受け取った関数が,「具体的にどんな型が指定されたか」を知る簡単な手段がないということです.ちなみに数だけは簡単にわかります.

先頭から切り出していきましょう

ということで,次のような戦略をとります.

template<class First, class... Rest>
void func(const First &first, const Rest&... rest);

こんなfuncがあるとき,

func<int, double, std::string>(10, 10.5, std::string("hoge"));
//func(10, 10.5, std::string("hoge"));  //実体化可能ならどっちでもいい

として実体化する(ここでは「呼び出す」と同義ですね)と,型としてはFirst=int,Rest={double, std::string}となり,値としてはfirst=10,rest={10.5, std::string(“hoge”)}となります.

Firstはちゃんとした型でありfirstはその値なので,これに対しては何なりと好きな処理を行えます.
型安全printfだと,実引数値を表示,といった形になります.

一方,Restは仮想的な型なので,型Restやその値restに対して何らかの処理を行うことはできません
これらに対して行えるのはunpackだけです.
これが前述した『「具体的にどんな型が指定されたか」を知る簡単な手段がない』ことの意味です.

ここで,前節で話した内容を使います.
つまり,次のような形で「再帰呼び出し」を行います.

//再帰終端用のダミー
void func()
{ }

template<class First, class... Rest>
void func(const First& first, const Rest&... rest)
{
    //まず,型や値の要素それぞれに対して行いたい実際の処理を,型Firstや値firstに対して行う.
    //例えばここでは値の表示.
    std::cout << first << std::endl;

    //再帰呼び出し
    func(rest...);    //値をunpackして渡す.

    //func<Rest...>(rest...);   //型も明示的にunpackして渡したらだめ!!
}

こうすると,再帰が深まる度に可変長テンプレート実引数の先頭から順に,行いたい処理ができます.

また,再帰の末端ではRestが空となり,funcの呼び出し時に該当関数が無いと怒られます.
ダミーの関数はこれを拾うためのものです.
ここでコメントでダメと書いたように型指定をすると,末端まではいいのですが,末端を拾う関数func()を呼び出す構文としては不正なので,エラーになります.

あと前方参照には気をつけましょうorz

さて,ここで大事なのは,このようなコード展開はコンパイル時に行われるということです.
なので型安全性も高いです.
前述したように,C#のparamsで実現できる可変長引数はこれで実現できます.
むしろC#のparamsでは任意の型を受け取れるようにするにはObject型への変換を伴い,なんやかんや問題が生じますが,
C++の可変長テンプレート引数に基づく可変長引数の関数では,型に対して直接個別の処理を行えるのでおいしいです.
(型変換は生じないし,型が必要な機能を持つかどうかの静的なチェックもできる)

ところで,上の実装はもうちょっと分かりやすく,というかmeaningfulな形にできますね.

template<class T>
void func(const T& t)
{
    std::cout << t << std::endl;
}

template<class First, class... Rest>
void func(const First& first, const Rest&... rest)
{
    func(first);
    func(rest...);
}

僕はこう書く方が好きです.

実行例をideoneにおいています. → http://ideone.com/1iwbD
ideoneはgcc4.5.1なんですね…あとC++0xのときはboostを使えないなど…
まさか今さらベタなforループを書くことになるとは思わなかった.
あ,このideoneのコード,vecの要素数が0の時は落ちますね,そこんとこは許してください…

で,実際にこの節で言ったような技法で型安全なprintfを書こうっていう話はC++0x – Wikipedia#可変長引数テンプレートにも載ってるので,そちらも併せてどうぞ,

本題2:関数に(テンプレート実引数で指定した型に対応する可変長の)引数が無い場合

さて,こちらは前述の,テンプレート実引数に対応した引数を受け取る関数に比べて,少々厄介となります.
というのも,引数が無い以上,実引数に基づく型推論が働き得ないからです.

このときに,型指定周りがめんどくさくなります.
単純に次のように書くと,エラーになります.

//このコードはエラー

void func()
{
    //表示する値はないので,型の名前を表示することにしましょう.出力内容は実装依存です.
    std::cout << typeid(First).name() << std::endl;
}

template<class First, class... Rest>
void func()
{
    func<First>();

    func<Rest...>();
    //↑
    //実引数が無いため型推論が使えず,テンプレート実引数は明示的に指定する必要がある
    //そうすると,再帰の末尾にたどり着いてRestが空となったとき,
    //呼び出し記述にマッチする関数が見つからない.
    //(そもそもこれにマッチする関数の定義自体がおそらく不可能?)
}

コメント中の説明に書いたような理由で,通りません.

といっても対策は(わざわざ新しい節で書くほどもないくらい)シンプルで,FirstだけじゃなくSecondも用意します.

template<class First>
void func()
{
    std::cout << typeid(First).name() << std::endl;
}

template<class First, class Second, class... Rest>
void func()
{
    func<First>();
    func<Second, Rest...>();
}

このアプローチはいわば「再帰の末端で問題が生じるなら,末端のひとつ手前でやめとこう」的な考え方です.
何が起こってるかは特に言わないでも大丈夫ですね.

実行結果はこちらです> http://ideone.com/cBzhy

環境について

gccにおいては可変長テンプレート引数周りは結構タイムリーに実装が進んでる領域のようで,特にクラステンプレートとしての可変長テンプレート引数の辺りはunimplementedがちらほらあります.

この記事でのサンプルは,gcc4.4.3とgcc4.5.1とgcc4.6.0でのそれぞれC++0xサポートで正しく動作することを確認しています.
ちなみに4.5.1はideoneです.

コメントを残す

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