こんにちは。田原です。

テンプレートの基本的な部分は概ね網羅しつつありますので、そろそろ当応用講座も終盤です。
さて、今回は可変長引数のテンプレートを解説します。再帰定義を使う場合も多く使い方は結構難しいですが、結構便利です。テンプレート・プログラミングしていると使いたくなることがたまにあると思いますので、解説してみます。

1.C言語時代からある可変長引数関数(Variadic functions)

標準ライブラリのprintf()関数などで可変長引数が使われています。これがあれば十分では?と思う方も少なくないと思いますので、まずはC言語時代からある可変長引数関数の問題点を説明します。

1-1.引数の型違いの問題

可変長引数関数は、呼び出し側は引数を自由に(好き勝手に)指定できます。問題はそれらの引数の型を呼び出された側に伝達する方法をC言語がサポートしていないことです。なので、呼ばれた側は呼び出し側が与えた型に応じた処理をすることが原則としてできません。
例えば、関数側はdoubleを期待しているのに呼び出す際にint型の値を渡すとエラーにならずに単に誤動作します。

// C++ではstdio.hの代わりにcstdioをインクルードします。中身は同じものです。
#include <cstdio>

int main()
{
    printf("%f\n", 123);
}
0.000000

気持ち的には123.000のような表示をして欲しいのですが、そうはなりません。
これは、printf()関数に”%f”として指示しているため、printf()関数は書式指定文字列の次にdoubleが来ることを期待しています。しかし、123とint側引数を与えたため、正常動作しません。

しかも、今回はたまたま0.000000になりましたが、常にこうなるわけではありません。gccの場合は引数の数が少ない時はレジスタで渡しており、整数型と浮動小数点型で使用するレジスタが異なるようですので、ちょっと実験です。

// C++ではstdio.hをcstdioとしてインクルードします(stdio.hでも大丈夫ですが。)
#include <cstdio>

int main()
{
    printf("%f\n", 987.654321);
    printf("%f\n", 123);
}
987.654321
987.654321

Wandboxで確認する。

この辺りの振る舞いは処理系依存です。Visual C++ではまた違った動作となります。

#include <cstdio>

int main()
{
    printf("%f\n", 0x3ff3c0c9539b8887llu);
}
1.234567

謎の0x3ff3c0c9539b8887llu
1.234567はdouble型ですのでVC++では8バイトで表現されます。同じくunsigned long long int型も8バイトで表現されます。そこで、次のようなコードで事前に表示してそれを使ったものです。
最後にlluというサフィックスを付けていますが、これはunsigned long long int型の定数という意味です。

    union
    {
        double d;
        unsigned long long int ulli;
    } data;
    data.d = 1.234567;
    printf("%llx\n", data.ulli);
3ff3c0c9539b8887

このように、VC++ではdoubleを期待しているところに整数値を与えると、それを無理やりdouble型と解釈して表示するようです。

1-2.コンパイラが警告してくれます

しかし、最近のコンパイラは優秀です。printf()が標準ライブラリにある関数であることを認識し、書式文字列と引数リストの型が一致しない場合、警告してくれます。
上記のケースではそれぞれ、次のような警告がでます。

prog.cc: In function 'int main()':
prog.cc:6:23: warning: format '%f' expects argument of type 'double', but argument 2 has type 'int' [-Wformat=]
     printf("%f\n", 123);
                       ^
test.cpp(5): warning C4477: 'printf' : 書式文字列 '%f' には、型 'double' の引数が必要ですが、可変個引数 1 は型 'unsigned __int64' です

1-3.しかし万能ではありません

printf()は標準ライブラリなので、コンパイラ(コンパイラの開発者)もその引数の意味を把握することができますが、自作関数の場合、そうはいきません。

例えば、ログ出力用に独自のprintf()的な関数を作ることは多いでしょう。手抜きですがちょっと作ってみました。(出力時刻を頭に付けています。)

#include <sstream>
#include <iomanip>
#include <cstdio>
#include <cstdarg>
#include <ctime>

void printLog(char const* format, ...)
{
    std::time_t now = std::time(nullptr);
    std::stringstream ss;
    ss << std::put_time(std::localtime(&now), "%T");
    printf("%s : ", ss.str().c_str());

    va_list arg;
    va_start(arg, format);
    vprintf(format, arg);
    va_end(arg);
}

int main()
{
    printLog("%f\n", 123);
}
14:23:17 : 0.000000

Wandboxで確認する。

警告なく例外も発生せずにシラッと異常動作します。一切のヒント無しにデバッグしないといけないため、なかなか嫌なものです。

std::localtime補足
未だに時刻を表示しようとするとC言語時代のライブラリに頼ることになります。
chronoが標準ライブラリに導入され、多少は時刻を扱えるのですが、日付を扱えずお手軽には表示できません。
しかも、localtimeは相変わらずスレッド安全ではないようです。
スレッド安全版について、入門編の第19回でちょっと触れていますので興味の有る方は参照下さい。

std::put_time補足
std::put_timeはstd::ostream用のマニピュレータです。iomanipをインクルードすると使えます。マニピュレータは書式を指定する際に良く使います。std::put_timeは時刻の書式指定という意味でマニピュレータなのかもしれません。(でも、std::stringを返してくれたほうが何倍もありがたい気がします。どうも時刻処理についてはC++の標準ライブラリは頂けません。)

2.C++11からの可変長引数テンプレート(Variadic templates)

C++11で可変長引数テンプレートが規定されました。関数テンプレートとクラス・テンプレートの両方で使えますが、今回は関数テンプレートを用いて解説します。

C言語標準ライブラリについてのコンパイラによるサポートは間違った指定をすると警告がでます。テンプレートの場合は呼び出し側で与えた引数の型が関数側に伝達されます。それを利用して関数テンプレートを書くプログラマが適切にプログラムできると言う考え方です。ですので、適切にコーディングしていれば、サポートする引数ならば適切な動作をしますし、不正な引数を与えた時にシラッと異常動作するのではなく、エラーにすることができます。(判りやすいエラー・メッセージにするのはたいへんですが、エラーにならないよりはましと思います。)

2-1.パラメータ・パック

パラメータ・パックは0個以上のパラメータの並びをまとめたものです。
この文脈で言うパラメータは2種類あります。テンプレート・パラメータと通常の関数のパラメータです。
両方ともパラメータ・パックがあります。

次の T はテンプレート・パラメータ・パック、 t は関数パラメータ・パックです。

#include <iostream>

template<typename... T>
void foo(T... t)
{
    std::cout << "sizeof...(T) = " << sizeof...(T) << " sizeof...(t) = " << sizeof...(t) << "\n";
}

int main()
{
    foo();
    foo(1);
    foo(1, 1.2, "three");
}

sizeof…(p)は、パラメータ・パックpに含まれるパラメータの数を返します。

sizeof...(T) = 0 sizeof...(t) = 0
sizeof...(T) = 1 sizeof...(t) = 1
sizeof...(T) = 3 sizeof...(t) = 3

…の付け方はちょっと判りにくいですが、ルールは3つですので覚えましょう。
1. template<>の中ではtypenameやclass、非型パラメータの型名の後ろに付きます。

  1. 関数の仮引数リストの中では、テンプレート・パラメータ・パックの後ろに付きます。
    この時、テンプレート・パラメータを加工してから展開することができます。後日解説します。

  2. 関数呼び出し時の実引数リストの中では、関数パラメータ・パックの後ろに付きます。
    この時、関数パラメータを使って処理してから展開することができます。後日解説します。

また、テンプレート・パラメータ・パックは1つのテンプレート仮引数には1つしか指定できません。
例えば2つ指定した場合、どこまでが前のパラメータ・パックに属するのか判断する術がないからです。

2-2.よくある使い方

パラメータ・パックは一塊ですから、1つ1つのパラメータを取り出して処理しないと行けない時はちょっと面倒です。ですが、纏めて処理できることは少ないです。独自にテンプレートを定義する際には概ね自力で展開する必要があります。
幾つか方法がありますが、ここでは最も基本的な再帰定義による方法を説明します。

先程のprintLog()を可変長引数テンプレートで実装し、型違いによる異常動作しないバージョンにしてみます。
ただし、書式指定に対応するのはかなり面倒ですので、書式指定不要とします。(書式と一致しなかった時にエラーにするなどしようとするとちょっと悪夢かも。)

#include <iostream>
#include <iomanip>
#include <ctime>

void printLogImpl()
{
}

template<typename tFirst, typename... tLeft>
void printLogImpl(tFirst first, tLeft... left)
{
    std::cout << first;
    if (sizeof...(left)) std::cout << ", ";
    printLogImpl(left...);
}

template<typename... T>
void printLog(T... t)
{
    std::time_t now = std::time(nullptr);
    std::cout << std::put_time(std::localtime(&now), "%T") << " : ";
    printLogImpl(t...);
}
 
int main()
{
    printLog(1, 1.2, "three");
}
19:02:38 : 1, 1.2, three

このように3つの関数を定義するパターンが多いです。

  1. 最初の入口となる関数テンプレートprintLog()
    パラメータ・パックを展開する前処理と後処理を記述したいケースが多いため、入口を別途用意することが多いです。前処理や後処理が不要な時は要りません。

  2. 再帰定義の途中を処理する関数テンプレートprintLogImpl()
    先頭のパラメータと残りのパラメータを分けて受け取ります。後者をパラメータ・パックで受け取ることで、先頭だけ分離して処理を行い、残りを処理するために自分自身のオーバーロード関数テンプレートを再帰的に呼び出します。
    (実際に呼ばれるものは自分自身と同じ実体化された関数ではないです。パラメータが1つ少なく実体化された関数が呼ばれます。)

  3. 再帰定義の最後を処理する通常のオーバーロード関数printLogImpl()
    再帰定義は、「最後」を if文で判断して「次」を呼び出さない処理をすることが一般的です。
    if文で判定することも不可能ではないですが虚しいので、オーバーロード解決で処理する場合が多いです。
    テンプレート・パラメータ・パックtLeftのパラメータ数が0の時、パラメータ無しのprintLogImpl()が呼ばれます。
    これは「次」を呼ばないのでここで再帰が終了するわけです。

Wandboxで確認する。
このWandboxの例では終了タイプ(END_TYPE)の相違で3つ用意してます。

END_TYPE == 0 : 上記サンプルと同じ
END_TYPE == 1 : 最後の1つになったところで再帰定義終了
END_TYPE == 2 : if文で終了判定

最後のケースは、「テンプレート・パラメータ・パックtLeftに含まれるパラメータの数」が0個の時はif文によりprintLogImpl()が呼ばれないようにしているのですが、実際には呼ばれなくても呼び出しコードがあるため呼び出すためのマシン語コードが生成されます。そのため、printLogImpl()が無かったらリンクエラーになります。つまり呼ばれないけど用意しておかないといけないのです。この辺が虚しいなと感じるところです。
実は、この問題はテンプレート・プログラミングしていると良くでてきます。if文で分岐して呼び出さないようにしても、呼び出し用のコードは生成されるため用意しておかないと行けないのです。今回のように簡単に用意できるものなら問題ないのですが、意外に面倒な場合もあります。

WandboxでEND_TYPEの値を書き換えて試してみて下さい。

3.まとめ

そういえばもう2018年です。時が経つのは速いです。2017年中に制定される筈だったC++17どうなったかな?と見てみたら、2017年の12月に正式なISOとして発行されてました。
江添亮の詳説C++17にて網羅的に解説されています。

個人的には下記が興味深いです。
1. クラス・テンプレートの型推論(コンストラクタで型推論するので冗長な記述が減りそう)
2. 構造化束縛で、複数の戻り値の受け取りが楽できそうです!!
3. inline変数ならextern宣言と実体定義を分けなくて良くなるようです。特にstaticメンバ変数の定義が楽に!!
4. ファイルシステムで、より高度なファイル処理を標準で処理できます。これはありがたい。
他にも多数の便利な機能や高度な機能が導入されているようです。びっくりの進化です。

さて、今回は可変長引数テンプレートの基本的な部分を解説しました。次回はもう少し応用的な部分(といってもそれ程凄いことまではできませんが)を解説します。お楽しみに!