その他の範囲

前の章では、主にint範囲を使用した。実際には、コンテナ、アルゴリズム、および範囲は、ほとんどの場合テンプレートとして実装される。その章のprint()の例もテンプレートでした。

void print(T)(T range) {
    // ...
}
D

print()の実装で欠けている点は、TInputRangeの一種である必要があるにもかかわらず、その要件をテンプレート制約で形式化していないことだ(テンプレート制約についてはテンプレートの詳細の章で説明した)。

std.rangeモジュールには、テンプレート制約とstatic if文の両方で役立つテンプレートが含まれている。

範囲種類テンプレート

isで始まる名前のテンプレートのグループは、型が特定の種類の範囲の要件を満たしているかどうかを判断する。例えば、isInputRange!Tは"TInputRangeであるか"という質問に答える。次のテンプレートは、型が特定の一般的な範囲の型であるかどうかを判断するためのものだ。

したがって、print()のテンプレート制約では、isInputRangeを使用できる。

void print(T)(T range)
        if (isInputRange!T) {
    // ...
}
D

他のテンプレートとは異なり、isOutputRangeは2つのテンプレートパラメータを取る。1つ目は範囲型、2つ目は要素型である。その範囲型がその要素型の出力を許可する場合、trueを返す。例えば、次の制約は、範囲がdouble要素を受け入れるOutputRangeでなければならないことを要求するものだ。

void foo(T)(T range)
        if (isOutputRange!(T, double)) {
    // ...
}
D

static ifと組み合わせて使用すると、これらの制約は、ユーザー定義の範囲の機能も決定することができる。例えば、ユーザー定義の範囲の依存範囲がForwardRangeである場合、ユーザー定義の範囲はその事実を利用して、save()関数も提供することができる。

これは、既存の範囲の要素の負の値(より正確には、要素の数値の補数)を生成する範囲で見てみよう。まずは、InputRange関数だけを見てみよう。

struct Negative(T)
        if (isInputRange!T) {
    T range;

    bool empty() {
        return range.empty;
    }

    auto front() {
        return -range.front;
    }

    void popFront() {
        range.popFront();
    }
}
D

注釈:後で説明するように、frontの戻り値の型はElementType!Tとしても指定できる。

この範囲の唯一の機能は、元の範囲の最初の要素の負の値を生成するfront関数にある。

通常どおり、この範囲に対応する便利な関数は次の通りである。

Negative!T negative(T)(T range) {
    return Negative!T(range);
}
D

この範囲は、前章で定義されたFibonacciSeriesなどと組み合わせて使用できる:

struct FibonacciSeries {
    int current = 0;
    int next = 1;

    enum empty = false;

    int front() const {
        return current;
    }

    void popFront() {
        const nextNext = current + next;
        current = next;
        next = nextNext;
    }

    FibonacciSeries save() const {
        return this;
    }
}

// ...

    writeln(FibonacciSeries().take(5).negative);
D

出力には、シリーズの最初の5つの要素の負の値が含まれる:

[0, -1, -1, -2, -3]

当然ながら、Negativeは単なるInputRangeであるため、ForwardRangeを必要とするcycle()のようなアルゴリズムでは使用できない:

writeln(FibonacciSeries()
        .take(5)
        .negative
        .cycle        // ← コンパイルエラー
        .take(10));
D

しかし、元の範囲がすでにForwardRangeである場合は、Negativesave()関数を提供しない理由はない。この条件は、static if文で判断でき、元の範囲がForwardRangeである場合は、save()を提供することができる。この場合、元の範囲のコピーで構築された新しいNegativeオブジェクトを返すだけで済む。

struct Negative(T)
        if (isInputRange!T) {
// ...

    static if (isForwardRange!T) {
        Negative save() {
            return Negative(range.save);
        }
    }
}
D

新しいsave()関数を追加すると、Negative!FibonacciSeriesForwardRangeになり、cycle()の呼び出しをコンパイルできるようになる。

writeln(FibonacciSeries()
        .take(5)
        .negative
        .cycle        // ← 現在コンパイルできる
        .take(10));
D

式全体の出力は、フィボナッチ数列の最初の5要素を取り、それらの負を取り、それらを無限に繰り返し、その要素のうち最初の10要素を取る、と表現できる。

[0, -1, -1, -2, -3, 0, -1, -1, -2, -3]

同じアプローチで、元の範囲がこれらの機能をサポートしている場合、NegativeBidirectionalRangeおよびRandomAccessRangeにすることができる。

struct Negative(T)
        if (isInputRange!T) {
// ...

    static if (isBidirectionalRange!T) {
        auto back() {
            return -range.back;
        }

        void popBack() {
            range.popBack();
        }
    }

    static if (isRandomAccessRange!T) {
        auto opIndex(size_t index) {
            return -range[index];
        }
    }
}
D

例えば、スライスで使用すると、[]演算子で負の要素にアクセスできる。

auto d = [ 1.5, 2.75 ];
auto n = d.negative;
writeln(n[1]);
D

出力:

-2.75
ElementTypeおよびElementEncodingType

ElementTypeは、範囲の要素の型を提供する。

例えば、次のテンプレート制約には、最初の範囲の要素型に関する要件が含まれている。

void foo(I1, I2, O)(I1 input1, I2 input2, O output)
        if (isInputRange!I1 &&
            isForwardRange!I2 &&
            isOutputRange!(O, ElementType!I1)) {
    // ...
}
D

前の制約はI1InputRangeI2ForwardRangeOI1の要素型を受け入れるOutputRangeであるかのように記述することができる

文字列は、実際の文字型に関係なく、常にUnicode文字の範囲であるため、常にdcharの範囲になる。つまり、ElementType!stringおよびElementType!wstringdcharになる。そのため、テンプレートで必要な場合、文字列範囲の実際のUTFエンコード型は、ElementEncodingTypeで取得できる。

その他の範囲テンプレート

std.rangeモジュールには、Dの他のコンパイル時機能と組み合わせて使用できる多くの範囲テンプレートが用意されている。以下は一例だ。

次の例では、isInfiniteを利用して、元の範囲が無限の場合にemptyenumとして提供し、Negative!Tも無限であることをコンパイル時に認識させる。

struct Negative(T)
        if (isInputRange!T) {
// ...

    static if (isInfinite!T) {
        // Negative!Tもまた無限である
        enum empty = false;

    } else {
        bool empty() {
            return range.empty;
        }
    }

// ...
}

static assert( isInfinite!(Negative!FibonacciSeries));
static assert(!isInfinite!(int[]));
D
inputRangeObject()を使用した実行時多態性outputRangeObject()

ほとんどテンプレートとして実装されている範囲は、コンパイル時の多態性を示す。この章および前の章の例では、この多態性を活用してきた。コンパイル時の多態性と実行時の多態性の違いについては、"テンプレートの詳細""コンパイル時の多態性"のセクションを参照。

コンパイル時多態性は、テンプレートのすべてのインスタンス化が異なる型であるという事実に対処する必要がある。例えば、take()テンプレートの戻り値の型は、元の範囲に直接関連している。

writeln(typeof([11, 22].negative.take(1)).stringof);
writeln(typeof(FibonacciSeries().take(1)).stringof);
D

出力:

Take!(Negative!(int[]))
Take!(FibonacciSeries)

この事実の当然の結果として、異なる範囲型は互いに割り当てることができない。以下は、2つのInputRange範囲間のこの非互換性の例だ。

auto range = [11, 22].negative;
// ... 後ほど ...
range = FibonacciSeries();    // ← コンパイルエラー
D

予想通り、コンパイルエラーはFibonacciSeriesNegative!(int[])が互換性がないことを示している:

エラー: FibonacciSeries型の式(FibonacciSeries(0, 1))を
Negative!(int[])に暗黙的に変換できない
Undefined

しかし、実際の範囲の型は異なるものの、どちらも intの範囲であるため、この非互換性は不必要な制限であると見なすことができる。使用上の観点からは、どちらの範囲も単にint要素を提供しているだけなので、それらの要素を生成する実際のメカニズムは重要ではないはずだ。

Phobosは、inputRangeObject()およびoutputRangeObject()によってこの問題に対処している。inputRangeObject()を使用すると、範囲を特定の要素の特定の型の特定の種類の範囲として表現することができる。この機能を使用すると、範囲の実際の型に関係なく、範囲を、例えば、 int要素のInputRangeとして使用することができる。

inputRangeObject()は、すべての非出力範囲(InputRangeForwardRangeBidirectionalRangeRandomAccessRange)をサポートする十分な柔軟性を持っている。この柔軟性のため、返されるオブジェクトはautoで定義できない。必要な範囲の正確な種類は明示的に指定する必要がある:

// "整数の入力範囲"を意味する:
InputRange!int range = [11, 22].negative.inputRangeObject;

// ... 後の部分で ...

// 以下の割り当ては現在コンパイルされる
range = FibonacciSeries().inputRangeObject;
D

別の例として、範囲を int要素のForwardRangeとして使用する必要がある場合、その型はForwardRange!intと明示的に指定する必要がある。

ForwardRange!int range = [11, 22].negative.inputRangeObject;

auto copy = range.save;

range = FibonacciSeries().inputRangeObject;
writeln(range.save.take(10));
D

この例では、ForwardRangeが実際に範囲として使用できることを証明するために、save()を呼び出している。

同様に、outputRangeObject()OutputRange範囲でも機能し、特定の型の要素を受け入れるOutputRangeとして使用することができる。

要約