More Ranges
We used mostly int
ranges in the previous chapter. In practice, containers, algorithms, and ranges are almost always implemented as templates. The print()
example in that chapter was a template as well:
void print(T)(T range) { // ... }
What lacks from the implementation of print()
is that even though it requires T
to be a kind of InputRange
, it does not formalize that requirement with a template constraint. (We have seen template constraints in the More Templates chapter.)
The std.range
module contains templates that are useful both in template constraints and in static if
statements.
Range kind templates
The group of templates with names starting with is
determine whether a type satisfies the requirements of a certain kind of range. For example, isInputRange!T
answers the question "is T
an InputRange
?" The following templates are for determining whether a type is of a specific general range kind:
Accordingly, the template constraint of print()
can use isInputRange
:
void print(T)(T range) if (isInputRange!T) { // ... }
Unlike the others, isOutputRange
takes two template parameters: The first one is a range type and the second one is an element type. It returns true
if that range type allows outputting that element type. For example, the following constraint is for requiring that the range must be an OutputRange
that accepts double
elements:
void foo(T)(T range) if (isOutputRange!(T, double)) { // ... }
When used in conjunction with static if
, these constraints can determine the capabilities of user-defined ranges as well. For example, when a dependent range of a user-defined range is a ForwardRange
, the user-defined range can take advantage of that fact and can provide the save()
function as well.
Let's see this on a range that produces the negatives of the elements of an existing range (more accurately, the numeric complements of the elements). Let's start with just the InputRange
functions:
struct Negative(T) if (isInputRange!T) { T range; bool empty() { return range.empty; } auto front() { return -range.front; } void popFront() { range.popFront(); } }
Note: As we will see below, the return type of front
can be specified as ElementType!T
as well.
The only functionality of this range is in the front
function where it produces the negative of the front element of the original range.
As usual, the following is the convenience function that goes with that range:
Negative!T negative(T)(T range) {
return Negative!T(range);
}
This range is ready to be used with e.g. FibonacciSeries
that was defined in the previous chapter:
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);
The output contains the negatives of the first five elements of the series:
[0, -1, -1, -2, -3]
Naturally, being just an InputRange
, Negative
cannot be used with algorithms like cycle()
that require a ForwardRange
:
writeln(FibonacciSeries()
.take(5)
.negative
.cycle // ← compilation ERROR
.take(10));
However, when the original range is already a ForwardRange
, there is no reason for Negative
not to provide the save()
function as well. This condition can be determined by a static if
statement and save()
can be provided if the original range is a ForwardRange
. In this case it is as trivial as returning a new Negative
object that is constructed by a copy of the original range:
struct Negative(T) if (isInputRange!T) { // ... static if (isForwardRange!T) { Negative save() { return Negative(range.save); } } }
The addition of the new save()
function makes Negative!FibonacciSeries
a ForwardRange
as well and the cycle()
call can now be compiled:
writeln(FibonacciSeries()
.take(5)
.negative
.cycle // ← now compiles
.take(10));
The output of the entire expression can be described as take the first five elements of the Fibonacci series, take their negatives, cycle those indefinitely, and take the first ten of those elements:
[0, -1, -1, -2, -3, 0, -1, -1, -2, -3]
With the same approach, Negative
can be made a BidirectionalRange
and a RandomAccessRange
if the original range supports those functionalities:
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]; } } }
For example, when it is used with a slice, the negative elements can be accessed by the []
operator:
auto d = [ 1.5, 2.75 ]; auto n = d.negative; writeln(n[1]);
The output:
-2.75
ElementType
and ElementEncodingType
ElementType
provides the types of the elements of the range.
For example, the following template constraint includes a requirement that is about the element type of the first range:
void foo(I1, I2, O)(I1 input1, I2 input2, O output) if (isInputRange!I1 && isForwardRange!I2 && isOutputRange!(O, ElementType!I1)) { // ... }
The previous constraint can be described as if I1
is an InputRange
and I2
is a ForwardRange
and O
is an OutputRange
that accepts the element type of I1
.
Since strings are always ranges of Unicode characters, regardless of their actual character types, they are always ranges of dchar
, which means that even ElementType!string
and ElementType!wstring
are dchar
. For that reason, when needed in a template, the actual UTF encoding type of a string range can be obtained by ElementEncodingType
.
More range templates
The std.range
module has many more range templates that can be used with D's other compile-time features. The following is a sampling:
-
isInfinite
: Whether the range is infinite -
hasLength
: Whether the range has alength
property -
hasSlicing
: Whether the range supports slicing i.e. witha[x..y]
-
hasAssignableElements
: Whether the return type offront
is assignable -
hasSwappableElements
: Whether the elements of the range are swappable e.g. withstd.algorithm.swap
-
hasMobileElements
: Whether the elements of the range are movable e.g. withstd.algorithm.move
This implies that the range has
moveFront()
,moveBack()
, ormoveAt()
, depending on the actual kind of the range. Since moving elements is usually faster than copying them, depending on the result ofhasMobileElements
a range can provide faster operations by callingmove()
. -
hasLvalueElements
: Whether the elements of the range are lvalues (roughly meaning that the elements are not copies of actual elements nor are temporary objects that are created on the fly)For example,
hasLvalueElements!FibonacciSeries
isfalse
because the elements ofFibonacciSeries
do not exist as themselves; rather, they are copies of the membercurrent
that is returned byfront
. Similarly,hasLvalueElements!(Negative!(int[]))
isfalse
because although theint
slice does have actual elements, the range that is represented byNegative
does not provide access to those elements; rather, it returns copies that have the negative signs of the elements of the actual slice. Conversely,hasLvalueElements!(int[])
istrue
because a slice provides access to actual elements of an array.
The following example takes advantage of isInfinite
to provide empty
as an enum
when the original range is infinite, making it known at compile time that Negative!T
is infinite as well:
struct Negative(T) if (isInputRange!T) { // ... static if (isInfinite!T) { // Negative!T is infinite as well enum empty = false; } else { bool empty() { return range.empty; } } // ... } static assert( isInfinite!(Negative!FibonacciSeries)); static assert(!isInfinite!(int[]));
Run-time polymorphism with inputRangeObject()
and outputRangeObject()
Being implemented mostly as templates, ranges exhibit compile-time polymorphism, which we have been taking advantage of in the examples of this chapter and previous chapters. (For differences between compile-time polymorphism and run-time polymorphism, see the "Compile-time polymorphism" section in the More Templates chapter.)
Compile-time polymorphism has to deal with the fact that every instantiation of a template is a different type. For example, the return type of the take()
template is directly related to the original range:
writeln(typeof([11, 22].negative.take(1)).stringof); writeln(typeof(FibonacciSeries().take(1)).stringof);
The output:
Take!(Negative!(int[])) Take!(FibonacciSeries)
A natural consequence of this fact is that different range types cannot be assigned to each other. The following is an example of this incompatibility between two InputRange
ranges:
auto range = [11, 22].negative; // ... at a later point ... range = FibonacciSeries(); // ← compilation ERROR
As expected, the compilation error indicates that FibonacciSeries
and Negative!(int[])
are not compatible:
Error: cannot implicitly convert expression (FibonacciSeries(0, 1)) of type FibonacciSeries to Negative!(int[])
However, although the actual types of the ranges are different, since they both are ranges of int
, this incompatibility can be seen as an unnecessary limitation. From the usage point of view, since both ranges simply provide int
elements, the actual mechanism that produces those elements should not be important.
Phobos helps with this issue by inputRangeObject()
and outputRangeObject()
. inputRangeObject()
allows presenting ranges as a specific kind of range of specific types of elements. With its help, a range can be used e.g. as an InputRange
of int
elements, regardless of the actual type of the range.
inputRangeObject()
is flexible enough to support all of the non-output ranges: InputRange
, ForwardRange
, BidirectionalRange
, and RandomAccessRange
. Because of that flexibility, the object that it returns cannot be defined by auto
. The exact kind of range that is required must be specified explicitly:
// Meaning "InputRange of ints": InputRange!int range = [11, 22].negative.inputRangeObject; // ... at a later point ... // The following assignment now compiles range = FibonacciSeries().inputRangeObject;
As another example, when the range needs to be used as a ForwardRange
of int
elements, its type must be specified explicitly as ForwardRange!int
:
ForwardRange!int range = [11, 22].negative.inputRangeObject; auto copy = range.save; range = FibonacciSeries().inputRangeObject; writeln(range.save.take(10));
The example calls save()
just to prove that the ranges can indeed be used as ForwardRange
ranges.
Similarly, outputRangeObject()
works with OutputRange
ranges and allows their use as an OutputRange
that accepts specific types of elements.
Summary
- The
std.range
module contains many useful range templates. - Some of those templates allow templates be more capable depending on the capabilities of original ranges.
inputRangeObject()
andoutputRangeObject()
provide run-time polymorphism, allowing using different types of ranges as specific kinds of ranges of specific types of elements.