可変個引数テンプレート
問題は簡単です: 任意個の任意の型の値を受け取って、 型ごとに適切な方法で一つ一つを表示する関数を書いて下さい。 例えば、以下のコードで
print(7, 'a', 6.8);
次のように表示するのが目標です。
7
'a'
6.8
まずは、これが標準C++でどう実現できるかを考えます。 次に、提案されている "可変個引数テンプレート" というC++の拡張を使ってみましょう。 最後に、D でこれを実現する様々な方法を紹介します。
C++ での解決策
標準 C++ の範囲内での方法
標準 C++ の範囲でこれをストレートに実現するには、 引数の数ごとに関数テンプレートを用意します。
#include <iostream>
using namespace::std;
void print()
{
}
template<class T1> void print(T1 a1)
{
cout << a1 << endl;
}
template<class T1, class T2> void print(T1 a1, T2 a2)
{
cout << a1 << endl;
cout << a2 << endl;
}
template<class T1, class T2, class T3> void print(T1 a1, T2 a2, T3 a3)
{
cout << a1 << endl;
cout << a2 << endl;
cout << a3 << endl;
}
... etc ...
これには複数の問題があります:
一つ。 関数を実装するときに、引数の上限を決めなければいけません。 実装者はたいてい、多めに見積もって 10個、あるいは20個のprint()をオーバーロードするでしょう。 にも関わらず、 誰かがそれよりもう一個多い引数を必要とする可能性はゼロではありません。 つまり、この方法は決して完全に正しい解となることはないのです。
二つ。関数テンプレートの中のロジックは、 カット&ペーストで増やしてから、 それぞれのテンプレートに合わせて注意深く修正することになります。 ロジックを少し変えようと思ったら、 全てのテンプレートに同じ変更を加える必要があり、 面倒ですしバグの元です。
三つ。関数オーバーロードを使う以上そうならざるをえませんが、 多数のテンプレートの間に明示的な視覚的つながりがなく、それぞれが独立しています。 これによって、コードが読みにくくなります。 実装者がそれぞれを一貫したスタイルで記述することに注意を払わなかった場合、 悲惨なことになります。
四つ。 ソースコードが肥大化し、コンパイル速度の低下を引き起こします。
C++ の拡張による方法
Douglas Gregor は、この問題を解決するために C++ に可変個引数テンプレート [1] を加えることを提案しています。 これを使ったコードは次のようになります:
void print()
{
}
template<class T, class... U> void print(T a1, U... an)
{
cout << a1 << newline;
print(an...);
}
引数をひとつひとつ取り出すために、 再帰的な関数テンプレートを使用しています。 引数なしの場合の特殊化版によって、再帰が停止します。 これはよくまとまった巧い解ですが、ひとつ無視できない問題があります。 それは、これが拡張機能の提案であり、C++標準の一部ではなく、 この形式でそのままC++標準に取り入れられる保証もなく、 そもそもどんな形式であれC++標準に入るかどうかもわかっておらず、 入ったとしても、 実際に普通のコンパイラに実装されるのが何年も何年も後になるかもしれない、ということです。
プログラミング言語 D による解決策
Dの、テンプレート無しだよ!な方法
C++ でこれをテンプレート無しでやろうというのは実用的ではありません。 Dならば可能です。 Dには型安全な可変個引数関数があるからです。
import std.stdio;
void print(...)
{
foreach (arg; _arguments)
{
writefx(stdout, (&arg)[0 .. 1], _argptr, 1);
auto size = arg.tsize();
_argptr += ((size + size_t.sizeof - 1) & ~(size_t.sizeof - 1));
}
}
特にエレガントでもなく、最も効率的というわけでもありませんが、 期待したとおりに動作しますし、 一個の関数にきちんとまとまっています。 (上記のコードでは、定義済みの引数 _argptr と _arguments を使っています。それぞれ、可変個引数の値の配列と型の配列を指すポインタです。)
C++ の可変個引数テンプレートによる方法のDへの直訳
D 言語の可変個引数テンプレートを使って、 C++の拡張版をそのままDに変換できます:
void print()()
{
}
void print(T, A...)(T t, A a)
{
writefln(t);
print(a);
}
二つの関数テンプレートを使います。一つめが引数がない場合のテンプレートで、 二つめの方による再帰を停止する役目も担っています。 二つめは引数を2個取ります。 先頭の値が入る t と、残りの値が入る a です。 A... という記法は引数が「タプル」であることを表していて、 暗黙のテンプレートインスタンス化によって、Aには、 tの後ろに来た値の型のリストがセットされます。つまり、print(7, 'a', 6.8) とすると T は int となり、A はタプル (char, double) となります。 変数 a は渡された引数式のタプルとなります。
この関数の動作は、まず最初の引数 t を表示し、 残りの引数 a を再帰的に自分自身に適用する、というものです。 引数がなくなったときには print()() が呼ばれ、再帰が停止します。
static if による方法
全てのロジックを一つの関数に納めるというのは良いアイデアです。 これを実現する一つの手としては、 static if を使った条件コンパイルがあります:
void print(A...)(A a)
{
static if (a.length)
{
writefln(a[0]);
static if (a.length > 1)
print(a[1 .. length]);
}
}
タプルは配列によく似た方法で操作できます。 従って、a.length はタプルaに入っている式の個数で、 a[0] はタプルの最初の式となります。 a[1 .. length] は元のタプル a のスライスとして新しいタプルを生成します。
foreach による方法
タプルは配列と同じように操作できるのですから、 タプルの内容を"ループ"する foreach 文を使うことができます:
void print(A...)(A a)
{
foreach(t; a)
writefln(t);
}
この一番最後のコードは実に単純で、それ一つで完結していて、 簡潔かつ効率的です。
謝辞
- 可変個引数がどのように動作すべきか、そしてその必要性について教えてくれた Andrei Alexandrescu に感謝します。
- Douglas Gregor, Jaakko Jaervi, Gary Powell による C++ の可変個引数テンプレート拡張 が発想の元となりました。感謝しています。