英語版
このページの英語版を見る

可変長のテンプレート

問題文はシンプルだ。 任意の型の値を任意の数だけ受け取り、 それらの値を型に適した方法で1行に1つずつ出力する関数を書く。 例えば、次のコードは

print(7, 'a', 6.8);

次のような出力になるはずである。

7
'a'
6.8

これをC++でどのように行うか見ていく。 次に、"プログラミング言語D"で可能なさまざまな方法で実行する。

C++による解決策

オーバーロードによる解決策

標準C++でこれを実行する最も簡単な方法は、 引数の数ごとに1つずつ、一連の関数テンプレートを使用することである。

#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 ...

これには重大な問題がある。

1つ目は、 関数実装者は関数の引数の最大数をあらかじめ決定しなければならない。 実装者は 通常、過剰なほどに余裕を見て作業を行うため、print()のオーバーロードを10個、あるいは20個も 作成することになる。しかし、どこかのユーザーがどうしてもう1つだけ引数を必要とする場合が ある。そのため、この解決策は決して完全に正しいとは言えない。

2つ目は、関数テンプレート本体のロジックを カット&ペーストで複製し、慎重に修正する必要がある 。関数テンプレートのすべてについて、ロジックを 調整する必要がある場合、それらの関数テンプレートすべてに 同じ調整を加えなければならないが、これは退屈でエラーが 起こりやすい。

3つ目は、関数のオーバーロードにありがちだが、 それらの関数には明白な視覚的な関連性はなく、それぞれが独立している。 これによりコードの理解が難しくなるが、 特に実装者がそれらの配置やフォーマットを一貫したスタイルで行わないと

4つ目は、ソースコードが肥大化し、コンパイルが遅くなることだ 。

C++ 可変長のテンプレート

C++11は可変長のテンプレートをサポートしている。

void print()
{
}

template<class T, class... U> void print(T a1, U... an)
{
    cout << a1 << newline;
    print(an...);
}

再帰関数テンプレートのインスタンス化を使用して 引数を一つずつ取得する。 引数なしの特殊化は再帰を終了する。

プログラミング言語 D ソリューション

D言語のルック・マ・ノー・テンプレート・ソリューション

現在のベストプラクティス、例えば標準ライブラリが推奨しているように、 可変個引数テンプレートソリューションが推奨されている。しかし、テンプレートソリューションが実用的でない場合、Dは TypeInfo システムを利用したランタイムソリューションを提供する(以下に例を示す)。CおよびC++が提供する 非テンプレート可変個引数関数(va_list )と()()的な類似性があるが、Dが提供するソリューションは 型安全であるのに対し、前述の代替案はそうではない。

import core.vararg;
import core.stdc.stdio;
/*
この関数はD1.0の一部であったが、現在は存在しない。この機能が
どのように使われるかの現実的な例を示すために、ここで復活させた。
*/
void doFormat(void delegate(dchar) putc, TypeInfo[] arguments, va_list argptr);
void print(...)
{
    void putc(dchar c)
    {
        fputc(c, stdout);
    }

    doFormat(&putc, _arguments, _argptr);
}

C++の可変長引数ソリューションをDに変換する

Dの可変長のテンプレートは、 C++11の可変長ソリューションの単純な変換を可能にする。

void print()
{
}

void print(T, A...)(T t, A a)
{
    import std.stdio;
    writeln(t);
    print(a);
}

2つのオーバーロードがある。最初のものは 引数がない場合の縮退ケースを提供し、2番目のものの再帰の終端となる。 2番目のものは2つの引数を持つ。最初の値にはt 、残りのすべての値にはa である。A... はパラメータがシーケンスであることを示し、 「暗黙の関数テンプレートインスタンス化」によりA にはt に続くすべての引数の型がすべて埋められる。したがって、print(7, 'a', 6.8)intT を、(char, double)A を代入する。 パラメータa は、t の後に渡された任意の引数の l値シーケンスである。 詳細は 「コンパイル時のシーケンス」を参照のこと。

この関数は、最初のパラメータt を表示し、 残りの引数とともに再帰的に自身を呼び出す a 。再帰は、引数がなくなるまでprint() を呼び出すことで終了する 。

"static if" ソリューション

すべてのロジックを単一の関数にカプセル化できれば良いのだが。 そのための一つの方法として、static ifを使用する方法がある。これは、条件付きコンパイルを提供する:

void print(A...)(A a)
{
    static if (a.length)
    {
        writeln(a[0]);
        static if (a.length > 1)
            print(a[1 .. $]);
    }
}

配列と同様にシーケンスを操作できる。 つまり、a.lengtha シーケンスの要素数に置き換わる。a[0] は シーケンスの最初の要素を返す。a[1 .. $] は 元のシーケンスの残りの要素から新しいシーケンスを作成する。

foreach による解決策

しかし、配列と同様にシーケンスも操作できるため、foreach 文を使用してシーケンスの要素を繰り返し処理することができる。

void print(A...)(A a)
{
    foreach(t; a)
        writeln(t);
}

最終的な結果は、驚くほどシンプルで、自己完結型でコンパクト かつ効率的である。

謝辞

  1. アンドレイ・アレクサンドレスク氏に感謝する。 可変長のテンプレートがどのように機能する必要があるか、また、それがなぜそれほど重要なのかを説明してくれた。
  2. C++の可変長テンプレートに関する素晴らしい研究を 行った Douglas Gregor、Jaakko Jaervi、Gary Powellに感謝する。