その他のテンプレート

テンプレートの章で、テンプレートの威力と利便性について見てきた。アルゴリズムやデータ構造を1つのテンプレートで定義しておけば、その定義を複数の型に使用することができる。

その章では、テンプレートの最も一般的な使用法、つまり関数、structclassテンプレート、およびそれらの型テンプレートパラメータでの使用法についてのみ説明した。この章では、テンプレートについてさらに詳しく説明する。先に進む前に、少なくともその章の要約セクションを確認しておくことをお勧めする。

ショートカット構文

Dのテンプレートは、強力であるだけでなく、定義や使用も簡単で、非常に読みやすい。関数、struct、またはclassテンプレートの定義は、テンプレートパラメータのリストを指定するだけで簡単だ。

T twice(T)(T value) {
    return 2 * value;
}

class Fraction(T) {
    T numerator;
    T denominator;

    // ...
}
D

上記のテンプレート定義は、Dのショートカットテンプレート構文を活用している。

完全な構文では、テンプレートはtemplateキーワードで定義される。上記の2つのテンプレート定義の対応する完全な構文は次の通りだ:

template twice(T) {
    T twice(T value) {
        return 2 * value;
    }
}

template Fraction(T) {
    class Fraction {
        T numerator;
        T denominator;

        // ...
    }
}
D

ほとんどのテンプレートはショートカット構文で定義されるが、コンパイラは常に完全な構文を使用する。コンパイラがショートカット構文を完全な形式に変換する際に、次のようなステップを実行していると想像できる:

  1. 定義をテンプレートブロックで囲む。
  2. そのブロックに同じ名前を付ける。
  3. テンプレートパラメータリストをテンプレートブロックに移動する。

これらのステップを経て到達した完全な構文は、同名テンプレートと呼ばれ、プログラマーが明示的に定義することもできる。同名テンプレートについては後で説明する。

テンプレート名前空間

テンプレートブロック内には複数の定義を含めることができる。次のテンプレートには、関数とstructの定義の両方が含まれている。

template MyTemplate(T) {
    T foo(T value) {
        return value / 3;
    }

    struct S {
        T member;
    }
}
D

特定の型に対してテンプレートをインスタンス化すると、ブロック内のすべての定義がインスタンス化される。次のコードは、intおよびdoubleに対してテンプレートをインスタンス化する。

auto result = MyTemplate!int.foo(42);
writeln(result);

auto s = MyTemplate!double.S(5.6);
writeln(s.member);
D

テンプレートの特定のインスタンス化は名前空間を導入する。インスタンス化内の定義は、その名前で参照できる。ただし、これらの名前が長すぎる場合は、aliasの章で見たようにエイリアスを使用できる:

alias MyStruct = MyTemplate!dchar.S;

// ...

auto o = MyStruct('a');
writeln(o.member);
D
同名テンプレート

同名テンプレートは、そのブロックと同じ名前を持つ定義を含むtemplateブロックだ。実際、各ショートカットテンプレート構文は、同名テンプレートのショートカットだ。

例として、20バイトを超える型を大きすぎるとして修飾する必要があるプログラムがあるとする。このような修飾は、テンプレートブロック内の定数bool値によって実現できる。

template isTooLarge(T) {
    enum isTooLarge = T.sizeof > 20;
}
D

テンプレートブロックとその唯一の定義の名前が同じであることに注意。この同名のテンプレートは、isTooLarge!int.isTooLarge全体の代わりにショートカット構文で使用される。

writeln(isTooLarge!int);
D

上記の強調表示部分は、ブロック内のbool値と同じである。intのサイズは 20 未満であるため、コードの出力はfalseになる。

この同名テンプレートは、ショートカット構文でも定義できる:

enum isTooLarge(T) = T.sizeof > 20;
D

同名のテンプレートは、特定の条件に応じて型エイリアスを定義する場合によく使われる。例えば、次の同名のテンプレートは、エイリアスを設定して2つの型のうち大きい方を選ぶ。

template LargerOf(A, B) {
    static if (A.sizeof < B.sizeof) {
        alias LargerOf = B;

    } else {
        alias LargerOf = A;
    }
}
D

longintよりも大きい(8バイト対4バイト)ため、LargerOf!(int, long)は型longと同じになる。このようなテンプレートは、2つの型自体がテンプレートパラメータである(またはテンプレートパラメータに依存している)他のテンプレートで特に役立つ。

// ...

/* この関数の戻り値の型は、その2つのテンプレートパラメーターの
 * いずれか大きい方だ: 型Aまたは型B。 */
auto calculate(A, B)(A a, B b) {
    LargerOf!(A, B) result;
    // ...
    return result;
}

void main() {
    auto f = calculate(1, 2L);
    static assert(is (typeof(f) == long));
}
D
templates_more.1
テンプレートの種類
関数、クラス、および構造体テンプレート

関数、classstructテンプレートについては、テンプレートの章で既に説明し、それ以降でも多くの例を見てきた。

メンバー関数テンプレート

structおよびclassのメンバー関数もテンプレートにすることができる。例えば、次のput()メンバー関数テンプレートは、その型がテンプレート内の操作と互換性がある限り、あらゆるパラメータ型で機能する(この特定のテンプレートの場合、stringに変換可能である必要がある)。

class Sink {
    string content;

    void put(T)(auto ref const T value) {
        import std.conv;
        content ~= value.to!string;
    }
}
D

ただし、テンプレートはインスタンス化回数が無限になる可能性があるため、コンパイラはインターフェースにどのテンプレートのインスタンスを含めるべきかを判断できないため、テンプレートは仮想関数にはできない。(したがって、abstractキーワードも使用できない。)

例えば、次のサブクラスにput()テンプレートが存在すると、関数がオーバーライドされているように見えるが、実際にはスーパークラスのput名が隠されている (エイリアスの章"名前の隠蔽"を参照)。

class Sink {
    string content;

    void put(T)(auto ref const T value) {
        import std.conv;
        content ~= value.to!string;
    }
}

class SpecialSink : Sink {
    /* 次のテンプレート定義は、スーパークラス
     * のテンプレートインスタンスをオーバーライドしない; これらの名前を
     * 非表示にする。 */
    void put(T)(auto ref const T value) {
        import std.string;
        super.put(format("{%s}", value));
    }
}

void fillSink(Sink sink) {
    /* 次の関数呼び出しは仮想ではない。
     * パラメータ'sink'は型'Sink'であるため、呼び出しは
     * 常にSinkの'put'テンプレートインスタンスに
     * ディスパッチされる。 */

    sink.put(42);
    sink.put("hello");
}

void main() {
    auto sink = new SpecialSink();
    fillSink(sink);

    import std.stdio;
    writeln(sink.content);
}
D
templates_more.2

その結果、オブジェクトは実際にはSpecialSinkだが、fillSink()内の両方の呼び出しはSinkにディスパッチされ、SpecialSink.put()が挿入する波括弧はコンテンツに含まれない:

42hello    ← SpecialSinkの動作ではなく、Sinkの動作
共用体テンプレート

共用体テンプレートは、構造体テンプレートと似ている。共用体テンプレートにも、ショートカット構文を使うことができる。

例として、共用体の章で見たIpAdress unionのより一般的なバージョンを設計しよう。そこでは、IPv4アドレスの値は、以前のバージョンのIpAdressuintメンバーとして保持され、セグメント配列の要素型はubyteだった。

union IpAddress {
    uint value;
    ubyte[4] bytes;
}
D

bytes配列は、IPv4アドレスの4つのセグメントへの簡単なアクセスを提供していた。

同じ概念は、以下のunionテンプレートのように、より一般的な方法で実装できる:

union SegmentedValue(ActualT, SegmentT) {
    ActualT value;
    SegmentT[/* セグメントの数 */] segments;
}
D

このテンプレートを使用すると、値とそのセグメントの型を自由に指定することができる。

必要なセグメントの数は、実際の値とセグメントの型によって異なる。IPv4アドレスには4つのubyteセグメントがあるため、その値は、IpAddressの以前の定義では4とハードコードされていた。SegmentedValueテンプレートでは、2つの特定の型に対してテンプレートがインスタンス化されるコンパイル時に、セグメントの数を計算する必要がある。

次の同名のテンプレートは、2つの型の.sizeofプロパティを利用して、必要なセグメントの数を計算している。

template segmentCount(ActualT, SegmentT) {
    enum segmentCount = ((ActualT.sizeof + (SegmentT.sizeof - 1))
                         / SegmentT.sizeof);
}
D
templates_more.3

ショートカット構文の方が読みやすいかもしれない:

enum segmentCount(ActualT, SegmentT) =
    ((ActualT.sizeof + (SegmentT.sizeof - 1))
     / SegmentT.sizeof);
D

注釈:SegmentT.sizeof - 1は、型のサイズが均等に分割できない場合に使用する。例えば、実際の型が5バイトで、セグメント型が2バイトの場合、合計3つのセグメントが必要だが、整数除算5/2の結果は2となり、不正確になる。

共用体テンプレートの定義はこれで完了。

union SegmentedValue(ActualT, SegmentT) {
    ActualT value;
    SegmentT[segmentCount!(ActualT, SegmentT)] segments;
}
D
templates_more.3

uintubyteのテンプレートのインスタンス化は、以前のIpAddressの定義と等価である:

import std.stdio;

void main() {
    auto address = SegmentedValue!(uint, ubyte)(0xc0a80102);

    foreach (octet; address.segments) {
        write(octet, ' ');
    }
}
D
templates_more.3

プログラムの出力は、共用体の章と同じだ。

21168192

このテンプレートの柔軟性を示すために、IPv4アドレスのパーツを2つのushort値としてアクセスする必要があると想像しよう。セグメント型としてushortを指定するだけで、簡単に実現できる。

auto address = SegmentedValue!(uint, ushort)(0xc0a80102);
D

IPv4 アドレスでは珍しいことだが、プログラムの出力は2つのushortセグメント値で構成される。

25849320
インターフェーステンプレート

インターフェーステンプレートは、インターフェースで使用される型(および固定長配列のサイズなどの値やインターフェースのその他の機能)に柔軟性を与える。

色のテンプレートパラメータによって色の型が決定される、色付きオブジェクト用のインターフェースを定義しよう。

interface ColoredObject(ColorT) {
    void paint(ColorT color);
}
D

このインターフェーステンプレートでは、そのサブタイプはpaint()関数を定義する必要があるが、色の型は柔軟に指定できる。

Webページ上のフレームを表すクラスは、赤、緑、青の3つの成分で表される色型を使用することを選択できる。

struct RGB {
    ubyte red;
    ubyte green;
    ubyte blue;
}

class PageFrame : ColoredObject!RGB {
    void paint(RGB color) {
        // ...
    }
}
D

一方、光の周波数を使用するクラスは、色を表すためにまったく別の型を選択することができる。

alias Frequency = double;

class Bulb : ColoredObject!Frequency {
    void paint(Frequency color) {
        // ...
    }
}
D

ただし、テンプレートの章で説明したように、"すべてのテンプレートのインスタンス化は、異なる型を生成する"ので、ColoredObject!RGBColoredObject!Frequencyは無関係のインターフェースであり、PageFrameBulbは無関係のクラスになる。

テンプレートパラメータの種類

これまで見てきたテンプレートパラメータは、すべて型のパラメータだった。これまで、TColorTなどのパラメータは、すべて型を表していた。例えば、Tは、テンプレートのインスタンス化に応じて、intdoubleStudentなどを意味していた。

テンプレートパラメータには、value、thisalias、およびtupleという他の種類もある。

型のテンプレートパラメータ

このセクションは、完全を期すために記載している。これまで見てきたテンプレートはすべて、型のパラメータを持っていた。

値のテンプレートパラメータ

値のテンプレートパラメータを使用すると、テンプレート実装で使用される特定の値を柔軟に変更することができる。

テンプレートはコンパイル時の機能であるため、値のテンプレートパラメータの値はコンパイル時に既知でなければならない。実行時に計算しなければならない値は使用できない。

値のテンプレートパラメータの利点を理解するために、幾何学的形状を表す一連の構造体から始めよう。

struct Triangle {
    Point[3] corners;
// ...
}

struct Rectangle {
    Point[4] corners;
// ...
}

struct Pentagon {
    Point[5] corners;
// ...
}
D

これらの型の他のメンバー変数およびメンバー関数はまったく同じであり、唯一の違いは角の数を決定するだけであると仮定しよう。

このような場合、値のテンプレートパラメータが役立つ。次の構造体テンプレートは、上記のすべての型などを表現するのに十分だ。

struct Polygon(size_t N) {
    Point[N] corners;
// ...
}
D

この構造体テンプレートの唯一のテンプレートパラメータは、size_t型の値Nである。値Nは、テンプレート内のどこでもコンパイル時の定数として使用できる。

このテンプレートは、任意の辺の長さを持つ形状を表現するのに十分な柔軟性がある:

auto centagon = Polygon!100();
D

以下のエイリアスは、前述の構造体定義に対応している。

alias Triangle = Polygon!3;
alias Rectangle = Polygon!4;
alias Pentagon = Polygon!5;

// ...

    auto triangle = Triangle();
    auto rectangle = Rectangle();
    auto pentagon = Pentagon();
D

上記の値テンプレートパラメータの型はsize_tだった。値がコンパイル時にわかる限り、値のテンプレートパラメータは、基本型、struct型、配列、文字列など、あらゆる型にすることができる。

struct S {
    int i;
}

// 構造体Sの値テンプレートパラメーター
void foo(S s)() {
    // ...
}

void main() {
    foo!(S(42))();    // リテラルS(42)を使用してインスタンス化
}
D
templates_more.4

次の例では、stringテンプレートパラメータを使用してXMLタグを表し、単純なXML出力を作成している。

例えば、位置 42を表すXMLタグは、<location>42</location>と出力される。

import std.string;

class XmlElement(string tag) {
    double value;

    this(double value) {
        this.value = value;
    }

    override string toString() const {
        return format("<%s>%s</%s>", tag, value, tag);
    }
}
D

テンプレートパラメータは、テンプレートの実装で使用される型ではなく、string の値に関するものであることに注意。その値は、テンプレート内のどこでもstringとして使うことができる。

プログラムが必要とするXML要素は、次のコードのようにエイリアスとして定義できる。

alias Location = XmlElement!"location";
alias Temperature = XmlElement!"temperature";
alias Weight = XmlElement!"weight";

void main() {
    Object[] elements;

    elements ~= new Location(1);
    elements ~= new Temperature(23);
    elements ~= new Weight(78);

    writeln(elements);
}
D
templates_more.5

出力:

[<location>1</location>, <temperature>23</temperature>, <weight>78</weight>]

値のテンプレートパラメータにもデフォルト値を設定することができる。例えば、次の構造体テンプレートは、デフォルトの次元数が3である多次元空間内の点を表している。

struct Point(T, size_t dimension = 3) {
    T[dimension] coordinates;
}
D

このテンプレートは、dimensionテンプレートパラメーターを指定せずに使用できる:

Point!double center;    // 3次元空間内の点
D

必要に応じて次元数を指定することもできる:

Point!(int, 2) point;   // 表面上のpoint
D

パラメータの数の可変性の章で、特別なキーワードは、コード内で使用される場合とデフォルトの関数引数として使用される場合で動作が異なることを説明した。

同様に、デフォルトのテンプレート引数として使用される特殊キーワードは、キーワードが表示されている場所ではなく、テンプレートがインスタンス化される場所を指す:

import std.stdio;

void func(T,
          string functionName = __FUNCTION__,
          string file = __FILE__,
          size_t line = __LINE__)(T parameter) {
    writefln("Instantiated at function %s at file %s, line %s.",
             functionName, file, line);
}

void main() {
    func(42);    // ← 行は14
}
D
templates_more.6

特別なキーワードはテンプレートの定義内に表示されるが、その値はテンプレートがインスタンス化される場所であるmain()を参照する。(訳注: 改変前のソースでは__LINE__は12行目にある)

ファイルdeneme.dの14行目のdeneme.main関数でインスタンス化。

以下では、多次元演算子オーバーロードの例で__FUNCTION__を使用する。

thisメンバー関数のテンプレートパラメータ

メンバー関数もテンプレートにすることができる。そのテンプレートパラメータは、他のテンプレートと同じ意味を持つ。

ただし、他のテンプレートとは異なり、メンバー関数のテンプレートパラメータはthisパラメータにもすることができる。その場合、thisキーワードの後に続く識別子は、オブジェクトのthis参照の正確な型を表する。(this参照は、コンストラクタでthis.member = valueとよく書かれるように、オブジェクト自体を意味する。

struct MyStruct(T) {
    void foo(this OwnType)() const {
        writeln("Type of this object: ", OwnType.stringof);
    }
}
D

OwnTypeテンプレートパラメータは、メンバー関数が呼び出されるオブジェクトの実際の型である。

auto m = MyStruct!int();
auto c = const(MyStruct!int)();
auto i = immutable(MyStruct!int)();

m.foo();
c.foo();
i.foo();
D

出力:

このオブジェクトの型MyStruct!int
const(MyStruct!int)
immutable(MyStruct!int)

ご覧のとおり、型には、Tの対応する型だけでなく、constimmutableなどの型修飾子も含まれる。

struct(またはclass)はテンプレートである必要はない。thisテンプレートパラメータは、テンプレート化されていない型のメンバー関数テンプレートにも出現することができる。

thisテンプレートパラメータは、2章後に説明するテンプレートミックスインでも役立つ。

aliasテンプレートパラメータ

aliasテンプレートパラメータは、プログラムで使用される任意のシンボルまたは式に対応することができる。このようなテンプレート引数の唯一の制約は、引数がテンプレート内での使用と互換性があることだ。

filter() map()は、aliasテンプレートパラメーターを使用して、実行する操作を決定する。

既存の変数を変更するためのstructテンプレートの簡単な例を見てみよう。structテンプレートは、aliasパラメータとして変数を受け取る。

struct MyStruct(alias variable) {
    void set(int value) {
        variable = value;
    }
}
D

メンバー関数は、そのパラメータを、structテンプレートがインスタンス化される変数に単に代入する。その変数は、テンプレートのインスタンス化時に指定する必要がある。

int x = 1;
int y = 2;

auto object = MyStruct!x();
object.set(10);
writeln("x: ", x, ", y: ", y);
D

そのインスタンス化において、variableテンプレートパラメーターは変数xに対応する:

xy
102

逆に、MyStruct!yでテンプレートをインスタンス化すると、variableyと関連付けられる。

次に、filter()map()に類似した、呼び出し可能なエンティティを表すaliasパラメーターを定義する:

void caller(alias func)() {
    write("calling: ");
    func();
}
D

()の括弧からわかるように、caller()はテンプレートパラメータを関数として使っている。さらに、括弧は空なので、引数を指定せずにこの関数を呼び出すことも可能でなければならない。

この説明に一致する次の2つの関数がある。これらは、テンプレート内でfunc()として呼び出すことができるため、どちらもfuncを表すことができる。

void foo() {
    writeln("foo called.");
}

void bar() {
    writeln("bar called.");
}
D

これらの関数は、caller()aliasパラメータとして使用できる。

caller!foo();
caller!bar();
D

出力:

呼び出し: fooが呼び出された。
呼び出し: barが呼び出された。

テンプレートでの使用方法と一致する限り、任意のシンボルをaliasパラメータとして使用できる。反例として、int変数をcaller()で使用すると、コンパイルエラーが発生する。

int variable;
caller!variable();    // ← コンパイルエラー
D

コンパイルエラーは、変数がテンプレートでの使用方法と一致しないことを示している:

エラー: ()の前に関数が必要、int型の変数ではない

この間違いはcaller!variableのインスタンス化にあるが、コンパイラにとってはvariableを関数として呼び出そうとしていることがエラーであるため、コンパイルエラーはcaller()テンプレート内のfunc()を指摘する。この問題に対処する1つの方法は、テンプレート制約を使用することだ。これについては、以下で説明する。

変数が、opCall()のオーバーロードがある、または関数リテラルであるなどの理由で関数呼び出し構文をサポートしている場合、caller()テンプレートでも正常に動作する。次の例は、この2つのケースの両方を示している。

class C {
    void opCall() {
        writeln("C.opCall called.");
    }
}

// ...

    auto o = new C();
    caller!o();

    caller!({ writeln("Function literal called."); })();
D

出力:

呼び出し: C.opCallが呼び出された。
呼び出し: 関数リテラルが呼び出された。

aliasパラメータも特殊化することができる。ただし、特殊化の構文は異なる。特殊化された型は、aliasキーワードとパラメータ名の間に指定する必要がある。

import std.stdio;

void foo(alias variable)() {
    writefln("The general definition is using '%s' of type %s.",
             variable.stringof, typeof(variable).stringof);
}

void foo(alias int i)() {
    writefln("The int specialization is using '%s'.",
             i.stringof);
}

void foo(alias double d)() {
    writefln("The double specialization is using '%s'.",
             d.stringof);
}

void main() {
    string name;
    foo!name();

    int count;
    foo!count();

    double length;
    foo!length();
}
D
templates_more.7

また、aliasパラメータは、テンプレート内で実際の変数の名前を使用可能にする点にも注意。

aliasパラメータを.stringofで変換した後の値(このプログラム内での結果)
一般的な定義name
intの特殊化count
doubleの特殊化length
タプルテンプレートパラメーター

可変長引数の章で、可変長引数関数は任意の数の任意の型の引数を受け取ることができることを説明した。例えば、writeln()は、任意の数の任意の型の引数で呼び出すことができる。

テンプレートも可変長である。名前の後に...が続くテンプレートパラメータは、そのパラメータの位置に任意の数および種類のパラメータを指定できる。このようなパラメータは、テンプレート内でタプルとして表示され、AliasSeqのように使用できる。

インスタンス化されるすべてのテンプレート引数に関する情報を単に表示するテンプレートを使って、この例を見てみよう。

void info(T...)(T args) {
    // ...
}
D
templates_more.8

テンプレートパラメータT...により、info可変長のテンプレートになる。Targsはどちらもタプルである。

次の例は、3つの異なる型の3つの値でその関数テンプレートをインスタンス化している。

import std.stdio;

// ...

void main() {
    info(1, "abc", 2.3);
}
D
templates_more.8

次の実装は、foreachループで引数を反復処理して、引数に関する情報を表示するだけだ:

void info(T...)(T args) {
    // 'args'はタプルのように使用されている:
    foreach (i, arg; args) {
        writefln("%s: %s argument %s",
                 i, typeof(arg).stringof, arg);
    }
}
D

注釈:前章で見たように、引数はタプルであるため、上記のforeach文はコンパイル時の foreachである

出力:

順番引数
0int1
1stringabc
2double2.3

typeof(arg)によって各引数の型を取得する代わりに、T[i]を使用することもできたことに注意。

関数テンプレートでは、テンプレート引数が推測できることはわかっている。そのため、前のプログラムでは、コンパイラは型をintstringdoubleと推測している。

しかし、テンプレートパラメータを明示的に指定することも可能だ。例えば、std.conv.toは、宛先型を明示的なテンプレートパラメータとして受け取る。

to!string(42);
D

テンプレートパラメータが明示的に指定されている場合、それらは値、型、およびその他の種類の混合であることができる。この柔軟性により、テンプレートの本体を適宜コーディングできるように、各テンプレートパラメータが型であるかどうかを判断できる必要がある。これは、引数をAliasSeqとして扱うことで実現される。

テキスト形式のソースコードとしてstruct定義を生成する関数テンプレートで、この例を見てみよう。この関数は、生成したソースコードをstringとして返すようにしよう。この関数は、まずstructの名前、その後に型とメンバーの名前をペアで指定して受け取ることができる。

import std.stdio;

void main() {
    writeln(structDefinition!("Student",
                              string, "name",
                              int, "id",
                              int[], "grades")());
}
D

このstructDefinitionのインスタンス化は、以下のstringを生成することが期待される:

struct Student {
    string name;
    int id;
    int[] grades;
}
D

注釈:ソースコードを生成する関数は、後で説明するキーワードmixinとともに使われる。

以下は、望ましい出力を生成する実装だ。関数テンプレートがis式をどのように利用しているかに注目。式is (arg)は、argが有効な型である場合にtrueを生成することを覚えておいてほしい。

import std.string;

string structDefinition(string name, Members...)() {
    /* メンバーはペアで指定する: まず型、
     * その次に名前。 */
    static assert((Members.length % 2) == 0,
                  "Members must be specified as pairs.");

    /* 構造体定義の最初の部分。 */
    string result = "struct " ~ name ~ "\n{\n";

    foreach (i, arg; Members) {
        static if (i % 2) {
            /* 奇数番号の引数はメンバーの名前である
             * 必要がある。ここでは名前を扱う代わりに、
             * 以下の'else'節でMembers[i+1]として
             * 使用する。
             *
             * 少なくとも、メンバー名が
             * 文字列として指定されていることを確認する。 */
            static assert(is (typeof(arg) == string),
                          "Member name " ~ arg.stringof ~
                          " is not a string.");

        } else {
            /* この場合、'arg'は
             * メンバーの型である。それが実際に型であることを確認する。 */
            static assert(is (arg),
                          arg.stringof ~ " is not a type.");

            /* 型とその名前からメンバー定義を
             * 生成する。
             *
             * 注釈: 以下のMembers[i]の代わりに
             * 'arg'と記述することもできる。 */
            result ~= format("    %s %s;\n",
                             Members[i].stringof, Members[i+1]);
        }
    }

    /* 構造体定義の閉じ括弧。 */
    result ~= "}";

    return result;
}

import std.stdio;

void main() {
    writeln(structDefinition!("Student",
                              string, "name",
                              int, "id",
                              int[], "grades")());
}
D
templates_more.9
typeof(this)typeof(super)、およびtypeof(return)

場合によっては、テンプレートの汎用性により、テンプレートコード内の特定の型を把握したり、記述したりすることが困難になることがある。このような場合、次の3つの特別なtypeofバリエーションが役立つ。これらは、この章で紹介するが、テンプレートではないコードでも機能する。

テンプレートの特化

テンプレートの章でテンプレートの特殊化について見た。型のパラメータと同様に、他の種類のテンプレートパラメータも特殊化することができる。以下は、テンプレートの一般的な定義と、0に対するその特殊化だ。

void foo(int value)() {
    // ... 一般的な定義 ...
}

void foo(int value : 0)() {
    // ... ゼロに関する特別な定義 ...
}
D

テンプレート特殊化は、以下のメタプログラミングのセクションで活用する。

メタプログラミング

コード生成に関する機能であるため、テンプレートはDの高度な機能の一つだ。テンプレートは、コードを生成するコードそのものだ。コードを生成するコードを書くことをメタプログラミングと呼ぶ。

テンプレートがコンパイル時機能であるため、通常は実行時に実行される一部の操作を、テンプレートのインスタンス化としてコンパイル時に移動することができる。

(注釈:コンパイル時関数実行(CTFE)も、同じ目的を実現する機能だ。CTFEについては、後の章で説明する。)

テンプレートをコンパイル時に実行する方法は、再帰的なテンプレートインスタンス化に基づいていることが一般的だ。

この例を見るために、まず、0から特定の値までの数字の合計を計算する通常の関数を考えてみよう。例えば、引数が4の場合、この関数は0+1+2+3+4の結果を返すはずだ。

int sum(int last) {
    int result = 0;

    foreach (value; 0 .. last + 1) {
        result += value;
    }

    return result;
}
D

これは、関数の反復的な実装だ。同じ関数は、再帰によっても実装できる。

int sum(int last) {
    return (last == 0
            ? last
            : last + sum(last - 1));
}
D

再帰関数は、最後の値と前の合計の和を返する。ご覧のとおり、この関数は0の値を特別に処理することで再帰を終了している。

関数は通常、実行時の機能だ。通常どおり、sum()は実行時に実行できる。

writeln(sum(4));
D

コンパイル時に結果が必要な場合、同じ計算を行う1つの方法は、関数テンプレートを定義することだ。この場合、パラメータは関数パラメータではなく、テンプレートパラメータでなければならない。

// 警告: このコードは正しくない。
int sum(int last)() {
    return (last == 0
            ? last
            : last + sum!(last - 1)());
}
D

この関数テンプレートは、last - 1によって自身をインスタンス化し、再帰によって合計を再度計算しようとする。しかし、このコードは間違っている。

三項演算子は実行時にコンパイルされるため、コンパイル時に再帰を終了する条件チェックがない:

writeln(sum!4());    // ← コンパイルエラー
D

コンパイラはテンプレートインスタンスが無限再帰を起こすことを検出し、任意の再帰回数で停止する:

エラー: テンプレートインスタンスdeneme.sum!(-296)の再帰的展開
Undefined

テンプレート引数4と-296の違いを考慮し、コンパイラはデフォルトでテンプレート展開を300で制限する。

メタプログラミングでは、再帰はテンプレート特殊化で終了する。0に対する次の特殊化は、期待される結果を生成する:

// 一般的な定義
int sum(int last)() {
    return last + sum!(last - 1)();
}

// ゼロの特別な定義
int sum(int last : 0)() {
    return 0;
}
D
templates_more.10

以下のプログラムは、sum()をテストするプログラムだ:

import std.stdio;

void main() {
    writeln(sum!4());
}
D

現在、プログラムは正常にコンパイルされ、4+3+2+1+0の結果を生成する:

10

ここで重要な点は、関数sum!4()は完全にコンパイル時に実行されることだ。コンパイルされたコードは、リテラル10writeln()を呼び出すことと同じだ。

writeln(10);         // writeln(sum!4())と同等
D

その結果、コンパイルされたコードは、可能な限り高速かつシンプルになる。値10は、4+3+2+1+0の結果として計算されるが、計算全体はコンパイル時に実行される。

前の例は、メタプログラミングの利点の一つである、実行時からのコンパイル時への操作の移動を示している。CTFEは、Dのメタプログラミングの慣用表現の一部を排除している。

コンパイル時多態性

オブジェクト指向プログラミング(OOP)では、多態性は継承によって実現される。例えば、関数がインターフェースを受け取る場合、その関数は、そのインターフェースを継承する任意のクラスのオブジェクトを受け入れる。

前の章で見た例を思い出してみよう。

import std.stdio;

interface SoundEmitter {
    string emitSound();
}

class Violin : SoundEmitter {
    string emitSound() {
        return "♩♪♪";
    }
}

class Bell : SoundEmitter {
    string emitSound() {
        return "ding";
    }
}

void useSoundEmittingObject(SoundEmitter object) {
    // ... いくつかの操作 ...
    writeln(object.emitSound());
    // ... さらに操作 ...
}

void main() {
    useSoundEmittingObject(new Violin);
    useSoundEmittingObject(new Bell);
}
D
templates_more.11

useSoundEmittingObject()は多態性の恩恵を受けている。SoundEmitterを受け取るため、そのインターフェースから派生したあらゆる型で使用することができる。

テンプレートは、あらゆる型を扱うことが本質的な機能であるため、一種の多態性を提供しているとも見ることができる。テンプレートが提供する多態性は、コンパイル時機能であるため、コンパイル時多態性と呼ばれる。一方、OOPの多態性は、実行時多態性と呼ばれる。

実際には、どちらの多態性も、型は特定の要件を満たさなければならないため、あらゆる型で使用できるわけではない。

実行時多態性では、型が特定のインターフェースを実装している必要がある。

コンパイル時多態性では、型がテンプレートによる使用方法と互換性がある必要がある。コードがコンパイルできる限り、テンプレート引数はそのテンプレートで使用できる。(注釈:引数は、オプションでテンプレート制約も満たす必要がある。テンプレート制約については、後で説明する。)

例えば、useSoundEmittingObject()が関数ではなく関数テンプレートとして実装されていた場合、object.emitSound()呼び出しをサポートするあらゆる型で使用することができる。

void useSoundEmittingObject(T)(T object) {
    // ... いくつかの操作 ...
    writeln(object.emitSound());
    // ... さらに操作 ...
}

class Car {
    string emitSound() {
        return "honk honk";
    }
}

// ...

    useSoundEmittingObject(new Violin);
    useSoundEmittingObject(new Bell);
    useSoundEmittingObject(new Car);
D

Carは他の型と継承関係はないが、コードは正常にコンパイルされ、各型のemitSound()メンバー関数が呼び出されることに注意。

コンパイル時の多態性は、実際の型よりも動作を重視するユーモラスな用語である"ダックタイピング"としても知られている。

コードの肥大化

コンパイラによって生成されるコードは、型のパラメータ、値のパラメータなどの引数ごとに異なる。

その理由は、intおよびdoubleを型テンプレート引数とみなすとわかる。各型は、異なる種類のCPUレジスタによって処理される必要がある。そのため、テンプレート引数が異なる場合は、同じテンプレートを別々にコンパイルする必要がある。つまり、コンパイラは、テンプレートのインスタンスごとに異なるコードを生成する必要がある。

例えば、useSoundEmittingObject()がテンプレートとして実装されている場合、そのインスタンス化の回数だけコンパイルされることになる。

これによりプログラムサイズが肥大化するため、この効果はコードの肥大化と呼ばれている。ほとんどのプログラムでは問題にならないが、テンプレートの効果として認識しておく必要がある。

一方、useSoundEmittingObject()のテンプレート化されていないバージョンでは、コードの繰り返しは発生しない。コンパイラは、その関数を1回だけコンパイルし、SoundEmitterインターフェースのすべての型に対して同じコードを実行する。実行時多態性では、異なる型に対して同じコードを異なる動作させることは、バックグラウンドでの関数ポインタによって実現されている。関数ポインタは実行時にわずかなコストがかかるが、ほとんどのプログラムではそのコストは重要ではない。

コードの肥大化と実行時多態性はどちらもプログラムの性能に影響を与えるため、特定のプログラムにおいて実行時多態性がコンパイル時多態性よりも適切なアプローチであるかどうかは事前に判断できない。

テンプレート制約

テンプレートは任意の引数でインスタンス化できるが、すべての引数がすべてのテンプレートと互換性があるわけではないという事実が不便さを生む。テンプレート引数が特定のテンプレートと互換性がない場合、その互換性の欠如は、その引数に対するテンプレートコードのコンパイル時に必ず検出される。その結果、コンパイルエラーはテンプレート実装内の行を指す。

useSoundEmittingObject()を、object.emitSound()の呼び出しをサポートしていない型で使用して、これを確認しよう。

class Cup {
    // ... emitSound()がない ...
}

// ...

    useSoundEmittingObject(new Cup);   // ← 互換性のない型
D

間違いなく、互換性のない型でテンプレートを使用しているコードにエラーがあるのだが、コンパイルエラーはテンプレート内の行を指している。

void useSoundEmittingObject(T)(T object) {
    // ... いくつかの操作 ...
    writeln(object.emitSound());    // ← コンパイルエラー
    // ... さらに操作 ...
}
D

望ましくない結果として、テンプレートがサードパーティのライブラリモジュールの一部である場合、コンパイルエラーはライブラリ自体の問題のように見える。

この問題は、インターフェースでは発生しないことに注意。インターフェースを受け取る関数は、そのインターフェースを実装する型でのみ呼び出すことができる。他の型でそのような関数を呼び出そうとすると、呼び出し元でコンパイルエラーになる。

テンプレート制約は、テンプレートの誤ったインスタンス化を禁止するためのものである。これらは、テンプレート本体の直前に、 条件として論理式で定義される。if

void foo(T)()
        if (/* ... 制約 ... */) {
    // ...
}
D

テンプレート定義は、その制約が特定のインスタンス化においてtrueとなる場合のみ、コンパイラによって考慮される。そうでない場合、そのテンプレート定義は、その使用において無視される。

テンプレートはコンパイル時の機能であるため、テンプレートの制約はコンパイル時に評価可能でなければならない。is式は、is式の章で見た式で、テンプレートの制約でよく使われる。以下の例でも、is式を使う。

単一要素のタプルパラメーター

テンプレートの単一のパラメータが、型、値、またはaliasの種類のいずれかである必要がある場合がある。これは、長さが1のタプルパラメータを使用することで実現できる。

template myTemplate(T...)
        if (T.length == 1) {
    static if (is (T[0])) {
        // 単一のパラメータは型
        enum bool myTemplate = /* ... */;

    } else {
        // 単一のパラメータは別の種類
        enum bool myTemplate = /* ... */;
    }
}
D

std.traitsモジュールのテンプレートの一部は、このイディオムを活用している。std.traitsについては後述する。

名前付き制約

制約が複雑で、テンプレートパラメータの要件を理解するのが難しい場合がある。この複雑さは、制約に名前を効果的に付けるイディオムによって処理できる。このイディオムは、Dの4つの機能、すなわち匿名関数、typeofis式、および同名テンプレートを組み合わせている。

型のパラメータを持つ関数テンプレートでこれを見てみよう。このテンプレートは、その関数パラメータを特定の方法で使用している。

void use(T)(T object) {
    // ...
    object.prepare();
    // ...
    object.fly(42);
    // ...
    object.land();
    // ...
}
D

テンプレートの実装から明らかなように、この関数が扱うことができる型は、オブジェクトに対して3つの特定の関数呼び出し(prepare()fly(42)land())をサポートしている必要がある。

その型に対するテンプレート制約を指定する1つの方法は、テンプレート内の各関数呼び出しに対して、isおよびtypeof式を使用することだ。

void use(T)(T object)
        if (is (typeof(object.prepare())) &&
            is (typeof(object.fly(1))) &&
            is (typeof(object.land()))) {
    // ...
}
D

この構文については、後で説明する。ここでは、is (typeof(object.prepare()))という構文全体が、その型が.prepare()呼び出しをサポートしているかどうかを意味すると考えておいて。

このような制約は目的を達成するが、読みづらい場合がある。代わりに、制約全体に説明的な名前を付けることもできる:

void use(T)(T object)
        if (canFlyAndLand!T) {
    // ...
}
D

この制約は、テンプレートが飛ぶことができる型と着陸できる型で動作するように設計されていることがより明確になったため、より読みやすくなっている。

このような制約は、以下の同名のテンプレートに類似したイディオムによって実現される:

template canFlyAndLand(T) {
    enum canFlyAndLand = is (typeof(
    {
        T object;
        object.prepare();  // 飛行の準備ができること
        object.fly(1);     // 一定距離飛行できること
        object.land();     // 着陸できること
    }()));
}
D

そのイディオムに参加するDの機能と、それらの相互作用は以下で説明される:

template canFlyAndLand(T) {
    //        (6)        (5)  (4)
    enum canFlyAndLand = is (typeof(
    { // (1)
        T object;         // (2)
        object.prepare();
        object.fly(1);
        object.land();
 // (3)
    }()));
}
D
  1. 匿名関数:匿名関数は、関数ポインタ、デリゲート、およびラムダの章で説明した。上記の強調表示された中括弧は、匿名関数を定義している。
  2. 関数ブロック:関数ブロックは、実際のテンプレートで使用される予定の型をそのまま使用する。まず、その型のオブジェクトが定義され、そのオブジェクトが特定の方法で使用される。(このコードは実行されない。以下を参照。)
  3. 関数の評価:匿名関数の末尾にある空の括弧は、通常、その関数を実行する。ただし、その呼び出し構文はtypeof内にあるため、実行されることはない。
  4. typeof式: typeofは、式の型を生成する。

    typeofの重要な点は、式を実行しないことだ。むしろ、その式が実行された場合に、その式の型を生成する。

    int i = 42;
    typeof(++i) j;    // 'int j;'と同じ
    
    assert(i == 42);  // ++iは実行されていない
    D

    前のassertが示すように、式++iは実行されていない。typeofは、その式が実行された場合にその式の型をintとして生成しただけである。

    typeofが受け取る式が有効でない場合、typeofは型をまったく生成しない (voidも生成しない)。したがって、canFlyAndLand内の匿名関数がTで正常にコンパイルできる場合、typeofは有効な型を生成する。それ以外の場合、型はまったく生成されない。

  5. is式: is式は、is式の章さまざまな使い方を紹介した。Type構文は、is (Type)構文が有効な場合にtrueを生成する:

    上記の2番目のtypeofは存在しないシンボルを受け取るが、コンパイラはコンパイルエラーを発生させない。むしろ、typeof式は型を生成しないため、is式はfalseを生成する。

    true
    false
  6. 同名テンプレート:上記のように、canFlyAndLandテンプレートには同じ名前の定義が含まれているため、テンプレートのインスタンス化はその定義自体になる。

最終的に、use()はより説明的な制約を獲得する:

void use(T)(T object)
        if (canFlyAndLand!T) {
    // ...
}
D

このテンプレートを、制約を満たす型と制約を満たさない型の2つの型で使用しよう。

// テンプレートの操作と一致する型
class ModelAirplane {
    void prepare() {
    }

    void fly(int distance) {
    }

    void land() {
    }
}

// テンプレートの操作と一致しない型
class Pigeon {
    void fly(int distance) {
    }
}

// ...

    use(new ModelAirplane);    // ← コンパイルできる 
    use(new Pigeon);           // ← コンパイルエラー
D

名前が付いていようがいまいが、テンプレートに制約があるため、コンパイルエラーはテンプレートが実装されている場所ではなく、使用されている行を指す。

多次元演算子オーバーロードでのテンプレートの使用

演算子オーバーロードの章で、opDollaropIndex、およびopSliceは要素のインデックス付けとスライスに使用されることを学んだ。1次元コレクションに対してオーバーロードされた場合、これらの演算子は次の役割を果たす。

これらの演算子関数には、テンプレート化されたバージョンもあり、上記のテンプレート化されていないものとは責任が異なる。特に、多次元演算子オーバーロードでは、opIndexopSliceの責任を引き継ぐことに注意。

opIndexAssign opIndexOpAssignにもテンプレート化されたバージョンがあり、コレクションの要素の範囲に対して操作を行う。

これらの演算子を定義するユーザー定義型は、多次元インデックスおよびスライス構文で使用できる。

			  // インデックスとスライシング引数で
          // 指定された要素に42を代入する:
          m[a, b..c, $-1, d..e] = 42;
//              ↑   ↑     ↑    ↑
// 寸法:         0   1     2    3
D

このような式は、まず演算子関数を呼び出す式に変換される。変換は、$文字をopDollar!dimension()への呼び出しに、インデックス範囲をopSlice!dimension(begin, end)への呼び出しに置き換えることで行われる。これらの呼び出しによって返される長さと範囲の情報は、opIndexAssignを呼び出す際の引数として使用される。したがって、上記の式は、次のように実行される(次元値は強調表示されている)。

// 上記と同等:
m.opIndexAssign(
    42,                    // ← 代入する値
    a,                     // ← dimension0の引数
    m.opSlice!1(b, c),     // ← dimension1の引数
    m.opDollar!2() - 1,    // ← dimension2の引数
    m.opSlice!3(d, e));    // ← dimension3の引数
D

したがって、opIndexAssignは引数から要素の範囲を決定する。

多次元の演算子オーバーロードの例

次のMatrixの例は、これらの演算子を2次元型に対してオーバーロードする方法を示している。

このコードは、より効率的な方法で実装できることに注意。例えば、単一の要素eに対して操作を行う場合でも、m[i, j]によって単一の要素のサブ行列を構築する代わりに、その要素に直接操作を適用することができる。

さらに、関数内のwriteln(__FUNCTION__)式は、コードの動作とは何の関係もない。これらは、さまざまな演算子の使用法でバックグラウンドで呼び出される関数を公開するためにのみ使用されている。

また、次元の値の正確さはテンプレート制約によって強制されていることに注意。

import std.stdio;
import std.format;
import std.string;

/* 2次元int配列として機能する。 */
struct Matrix {
private:

    int[][] rows;

    /* 行または列の範囲を表す。 */
    struct Range {
        size_t begin;
        size_t end;
    }

    /* 行と列の範囲で指定されたサブマトリックスを
     * 返す。 */
    Matrix subMatrix(Range rowRange, Range columnRange) {
        writeln(__FUNCTION__);

        int[][] slices;

        foreach (row; rows[rowRange.begin .. rowRange.end]) {
            slices ~= row[columnRange.begin .. columnRange.end];
        }

        return Matrix(slices);
    }

public:

    this(size_t height, size_t width) {
        writeln(__FUNCTION__);

        rows = new int[][](height, width);
    }

    this(int[][] rows) {
        writeln(__FUNCTION__);

        this.rows = rows;
    }

    void toString(void delegate(const(char)[]) sink) const {
        sink.formattedWrite!"%(%(%5s %)\n%)"(rows);
    }

    /* 指定した値を、行列の各要素に
     * 割り当てる。 */
    Matrix opAssign(int value) {
        writeln(__FUNCTION__);

        foreach (row; rows) {
            row[] = value;
        }

        return this;
    }

    /* 各要素と値を2進演算で使用し、
     * その結果をその要素に割り当てる。 */
    Matrix opOpAssign(string op)(int value) {
        writeln(__FUNCTION__);

        foreach (row; rows) {
            mixin ("row[] " ~ op ~ "= value;");
        }

        return this;
    }

    /* 指定した次元の長さを返す。 */
    size_t opDollar(size_t dimension)() const
            if (dimension <= 1) {
        writeln(__FUNCTION__);

        static if (dimension == 0) {
            /* 次元0の長さは、
             * 'rows'配列の長さである。 */
            return rows.length;

        } else {
            /* 次元1の長さは、'rows'の
             * 要素の長さだ。 */
            return rows.length ? rows[0].length : 0;
        }
    }

    /* 'begin'から'end'までの範囲を表す
     * オブジェクトを返す。
     *
     * 注釈: テンプレートパラメータ'dimension'は
     * ここでは使用されていないが、その情報は他の型で
     * 役立つ場合がある。 */
    Range opSlice(size_t dimension)(size_t begin, size_t end)
            if (dimension <= 1) {
        writeln(__FUNCTION__);

        return Range(begin, end);
    }

    /* 引数で定義されたサブマトリックスを
     * 返す。 */
    Matrix opIndex(A...)(A arguments)
            if (A.length <= 2) {
        writeln(__FUNCTION__);

        /* まず、マトリックス全体を表す範囲から始める。
         * これにより、パラメーターなしのopIndexは
         * "すべての要素"を意味する。 */
        Range[2] ranges = [ Range(0, opDollar!0),
                            Range(0, opDollar!1) ];

        foreach (dimension, a; arguments) {
            static if (is (typeof(a) == Range)) {
                /* この次元は既に'matrix[begin..end]'のような
                 * 範囲として指定されており、
                 * そのまま使用できる。 */
                ranges[dimension] = a;

            } else static if (is (typeof(a) : size_t)) {
                /* この次元は、'matrix[i]'のような
                 * 単一のインデックス値として指定されており、
                 * 単一の要素の範囲として表現したい。 */
                ranges[dimension] = Range(a, a + 1);

            } else {
                /* 他の型は想定していない。 */
                static assert(
                    false, format("Invalid index type: %s",
                                  typeof(a).stringof));
            }
        }

        /* 'arguments'で指定された部分行列を
         * 返す。 */
        return subMatrix(ranges[0], ranges[1]);
    }

    /* 指定された値を部分行列の各要素に
     * 割り当てる。 */
    Matrix opIndexAssign(A...)(int value, A arguments)
            if (A.length <= 2) {
        writeln(__FUNCTION__);

        Matrix subMatrix = opIndex(arguments);
        return subMatrix = value;
    }

    /* サブ行列の各要素と値を
     * 二項演算で使用し、その結果をその
     * 要素に代入する。 */
    Matrix opIndexOpAssign(string op, A...)(int value,
                                            A arguments)
            if (A.length <= 2) {
        writeln(__FUNCTION__);

        Matrix subMatrix = opIndex(arguments);
        mixin ("return subMatrix " ~ op ~ "= value;");
    }
}

/* 文字列として指定された式を実行し、
 * 結果と行列の新しい状態を
 * 表示する。 */
void execute(string expression)(Matrix m) {
    writefln("\n--- %s ---", expression);
    mixin ("auto result = " ~ expression ~ ";");
    writefln("result:\n%s", result);
    writefln("m:\n%s", m);
}

void main() {
    enum height = 10;
    enum width = 8;

    auto m = Matrix(height, width);

    int counter = 0;
    foreach (row; 0 .. height) {
        foreach (column; 0 .. width) {
            writefln("Initializing %s of %s",
                     counter + 1, height * width);

            m[row, column] = counter;
            ++counter;
        }
    }

    writeln(m);

    execute!("m[1, 1] = 42")(m);
    execute!("m[0, 1 .. $] = 43")(m);
    execute!("m[0 .. $, 3] = 44")(m);
    execute!("m[$-4 .. $-1, $-4 .. $-1] = 7")(m);

    execute!("m[1, 1] *= 2")(m);
    execute!("m[0, 1 .. $] *= 4")(m);
    execute!("m[0 .. $, 0] *= 10")(m);
    execute!("m[$-4 .. $-2, $-4 .. $-2] -= 666")(m);

    execute!("m[1, 1]")(m);
    execute!("m[2, 0 .. $]")(m);
    execute!("m[0 .. $, 2]")(m);
    execute!("m[0 .. $ / 2, 0 .. $ / 2]")(m);

    execute!("++m[1..3, 1..3]")(m);
    execute!("--m[2..5, 2..5]")(m);

    execute!("m[]")(m);
    execute!("m[] = 20")(m);
    execute!("m[] /= 4")(m);
    execute!("(m[] += 5) /= 10")(m);
}
D
templates_more.12
要約

以前のテンプレート章では、以下の点が再確認された:

この章では、以下の概念が追加された。