We have seen the power and convenience of templates in the Templates chapter. A single templated definition of an algorithm or a data structure is sufficient to use that definition for multiple types.
That chapter covered only the most common uses of templates: function, struct, and class templates and their uses with type template parameters. In this chapter we will see templates in more detail. Before going further, I recommend that you review at least the summary section of that chapter.
78.1 The shortcut syntax
In addition to being powerful, D templates are easy to define and use and they are very readable. Defining a function, struct, or class template is as simple as providing a template parameter list:
T twice(T)(T value) {
return 2 * value;
}
class Fraction(T) {
T numerator;
T denominator;
// ...
}
Template definitions like the ones above are taking advantage of D's shortcut template syntax.
In their full syntax, templates are defined by the template keyword. The equivalents of the two template definitions above are the following:
在完整的语法中,模板是由
template关键字定义的。
template twice(T) {
T twice(T value) {
return 2 * value;
}
}
template Fraction(T) {
class Fraction {
T numerator;
T denominator;
// ...
}
}
Although most templates are defined by the shortcut syntax, the compiler always uses the full syntax. We can imagine the compiler applying the following steps to convert a shortcut syntax to its full form behind the scenes:
- Wrap the definition with a
templateblock. - Give the same name to that block.
- Move the
templateparameter list to thetemplateblock.
The full syntax that is arrived after those steps is called an eponymous template, which the programmer can define explicitly as well. We will see eponymous templates later below.
Template name space
It is possible to have more than one definition inside a template block. The following template contains both a function and a struct definition:
在一个模板块中可以有多个定义。
template MyTemplate(T) {
T foo(T value) {
return value / 3;
}
struct S {
T member;
}
}
Instantiating the template for a specific type instantiates all of the definitions inside the block. The following code instantiates the template for int and double:
auto result = MyTemplate!int.foo(42);
writeln(result);
auto s = MyTemplate!double.S(5.6);
writeln(s.member);
A specific instantiation of a template introduces a name space. The definitions that are inside an instantiation can be used by that name. However, if these names are too long, it is always possible to use aliases as we have seen in the alias chapter:
alias MyStruct = MyTemplate!dchar.S;
// ...
auto o = MyStruct('a');
writeln(o.member);
Eponymous templates
Eponymous templates are template blocks that contain a definition that has the same name as that block. In fact, each shortcut template syntax is the shortcut of an eponymous template.
同名模板是包含与该模板块同名的定义的模板块。事实上,每个快捷模板语法都是同名模板的快捷方式。
As an example, assume that a program needs to qualify types that are larger than 20 bytes as too large. Such a qualification can be achieved by a constant bool value inside a template block:
template isTooLarge(T) {
enum isTooLarge = T.sizeof > 20;
}
Note how the names of both the template block and its only definition are the same. This eponymous template is used by the shortcut syntax instead of the whole isTooLarge!int.isTooLarge:
writeln(isTooLarge!int);
The highlighted part above is the same as the bool value inside the block. Since the size of int is less than 20, the output of the code would be false.
That eponymous template can be defined by the shortcut syntax as well:
这个同名的模板也可以用快捷语法定义。
enum isTooLarge(T) = T.sizeof > 20;
A common use of eponymous templates is defining type aliases depending on certain conditions. For example, the following eponymous template picks the larger of two types by setting an alias to it:
同名模板的一个常见用法是根据特定条件定义类型别名。
template LargerOf(A, B) {
static if (A.sizeof < B.sizeof) {
alias LargerOf = B;
} else {
alias LargerOf = A;
}
}
Since long is larger than int (8 bytes versus 4 bytes), LargerOf!(int, long) would be the same as the type long. Such templates are especially useful in other templates where the two types are template parameters themselves (or depend on template parameters):
// ...
/* The return type of this function is the larger of its two
* template parameters: Either type A or type 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));
}
78.2 Kinds of templates
Function, class, and struct templates
We have already covered function, class, and struct templates in the Templates chapter and we have seen many examples of them since then.
Member function templates
struct and class member functions can be templates as well. For example, the following put() member function template would work with any parameter type as long as that type is compatible with the operations inside the template (for this specific template, it should be convertible to string):
class Sink {
string content;
void put(T)(auto ref const T value) {
import std.conv;
content ~= value.to!string;
}
}
However, as templates can have potentially infinite number of instantiations, they cannot be virtual functions because the compiler cannot know which specific instantiations of a template to include in the interface. (Accordingly, the abstract keyword cannot be used either.)
然而,由于模板可能有无限个实例化,它们不能是虚函数,因为编译器不知道该在接口中包含模板的哪些特定实例化。(因此,
abstract关键字也不能使用)
For example, although the presence of the put() template in the following subclass may give the impression that it is overriding a function, it actually hides the put name of the superclass (see name hiding in the alias chapter):
class Sink {
string content;
void put(T)(auto ref const T value) {
import std.conv;
content ~= value.to!string;
}
}
class SpecialSink : Sink {
/* The following template definition does not override
* the template instances of the superclass; it hides
* those names. */
void put(T)(auto ref const T value) {
import std.string;
super.put(format("{%s}", value));
}
}
void fillSink(Sink sink) {
/* The following function calls are not virtual. Because
* parameter 'sink' is of type 'Sink', the calls will
* always be dispatched to Sink's 'put' template
* instances. */
sink.put(42);
sink.put("hello");
}
void main() {
auto sink = new SpecialSink();
fillSink(sink);
import std.stdio;
writeln(sink.content);
}
As a result, although the object actually is a SpecialSink, both of the calls inside fillSink() are dispatched to Sink and the content does not contain the curly brackets that SpecialSink.put() inserts:
42hello ← Sink's behavior, not SpecialSink's
Union templates
Union templates are similar to struct templates. The shortcut syntax is available for them as well.
As an example, let's design a more general version of the IpAdress union that we saw in the Unions chapter. There, the value of the IPv4 address was kept as a uint member in that earlier version of IpAdress, and the element type of the segment array was ubyte:
union IpAddress {
uint value;
ubyte[4] bytes;
}
The bytes array provided easy access to the four segments of the IPv4 address.
The same concept can be implemented in a more general way as the following union template:
union SegmentedValue(ActualT, SegmentT) {
ActualT value;
SegmentT[/* number of segments */] segments;
}
That template would allow specifying the types of the value and its segments freely.
The number of segments that are needed depends on the types of the actual value and the segments. Since an IPv4 address has four ubyte segments, that value was hard-coded as 4 in the earlier definition of IpAddress. For the SegmentedValue template, the number of segments must be computed at compile time when the template is instantiated for the two specific types.
The following eponymous template takes advantage of the .sizeof properties of the two types to calculate the number of segments needed:
template segmentCount(ActualT, SegmentT) {
enum segmentCount = ((ActualT.sizeof + (SegmentT.sizeof - 1))
/ SegmentT.sizeof);
}
The shortcut syntax may be more readable:
enum segmentCount(ActualT, SegmentT) =
((ActualT.sizeof + (SegmentT.sizeof - 1))
/ SegmentT.sizeof);
Note: The expression SegmentT.sizeof - 1 is for when the sizes of the types cannot be divided evenly. For example, when the actual type is 5 bytes and the segment type is 2 bytes, even though a total of 3 segments are needed, the result of the integer division 5/2 would incorrectly be 2.
The definition of the union template is now complete:
union SegmentedValue(ActualT, SegmentT) {
ActualT value;
SegmentT[segmentCount!(ActualT, SegmentT)] segments;
}
Instantiation of the template for uint and ubyte would be the equivalent of the earlier definition of IpAddress:
import std.stdio;
void main() {
auto address = SegmentedValue!(uint, ubyte)(0xc0a80102);
foreach (octet; address.segments) {
write(octet, ' ');
}
}
The output of the program is the same as the one in the Unions chapter:
2 1 168 192
To demonstrate the flexibility of this template, let's imagine that it is required to access the parts of the IPv4 address as two ushort values. It would be as easy as providing ushort as the segment type:
auto address = SegmentedValue!(uint, ushort)(0xc0a80102);
Although unusual for an IPv4 address, the output of the program would consist of two ushort segment values:
258 49320
Interface templates
Interface templates provide flexibility on the types that are used on an interface (as well as values such as sizes of fixed-length arrays and other features of an interface).
Let's define an interface for colored objects where the type of the color is determined by a template parameter:
interface ColoredObject(ColorT) {
void paint(ColorT color);
}
That interface template requires that its subtypes must define the paint() function but it leaves the type of the color flexible.
A class that represents a frame on a web page may choose to use a color type that is represented by its red, green, and blue components:
struct RGB {
ubyte red;
ubyte green;
ubyte blue;
}
class PageFrame : ColoredObject!RGB {
void paint(RGB color) {
// ...
}
}
On the other hand, a class that uses the frequency of light can choose a completely different type to represent color:
alias Frequency = double;
class Bulb : ColoredObject!Frequency {
void paint(Frequency color) {
// ...
}
}
However, as explained in the Templates chapter, "every template instantiation yields a distinct type". Accordingly, the interfaces ColoredObject!RGB and ColoredObject!Frequency are unrelated interfaces, and PageFrame and Bulb are unrelated classes.
78.3 Kinds of template parameters
The template parameters that we have seen so far have all been type parameters. So far, parameters like T and ColorT all represented types. For example, T meant int, double, Student, etc. depending on the instantiation of the template.
There are other kinds of template parameters: value, this, alias, and tuple.
Type template parameters
This section is only for completeness. All of the templates that we have seen so far had type parameters.
Value template parameters
Value template parameters allow flexibility on certain values used in the template implementation.
值模板参数允许在模板实现中灵活使用某些值。
Since templates are a compile-time feature, the values for the value template parameters must be known at compile time; values that must be calculated at run time cannot be used.
由于模板是一个编译时特性,值模板参数的值必须在编译时知道;不能使用必须在运行时计算的值。
To see the advantage of value template parameters, let's start with a set of structs representing geometric shapes:
struct Triangle {
Point[3] corners;
// ...
}
struct Rectangle {
Point[4] corners;
// ...
}
struct Pentagon {
Point[5] corners;
// ...
}
Let's assume that other member variables and member functions of those types are exactly the same and that the only difference is the value that determines the number of corners.
Value template parameters help in such cases. The following struct template is sufficient to represent all of the types above and more:
struct Polygon(size_t N) {
Point[N] corners;
// ...
}
The only template parameter of that struct template is a value named N of type size_t. The value N can be used as a compile-time constant anywhere inside the template.
That template is flexible enough to represent shapes of any sides:
auto centagon = Polygon!100();
The following aliases correspond to the earlier struct definitions:
alias Triangle = Polygon!3;
alias Rectangle = Polygon!4;
alias Pentagon = Polygon!5;
// ...
auto triangle = Triangle();
auto rectangle = Rectangle();
auto pentagon = Pentagon();
The type of the value template parameter above was size_t. As long as the value can be known at compile time, a value template parameter can be of any type: a fundamental type, a struct type, an array, a string, etc.
struct S {
int i;
}
// Value template parameter of struct S
void foo(S s)() {
// ...
}
void main() {
foo!(S(42))(); // Instantiating with literal S(42)
}
The following example uses a string template parameter to represent an XML tag to produce a simple XML output:
- First the tag between the
< >characters: ` - Then the value
- Finally the tag between the
</ >characters:</tag>
For example, an XML tag representing location 42 would be printed as <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);
}
}
Note that the template parameter is not about a type that is used in the implementation of the template, rather it is about a string value. That value can be used anywhere inside the template as a string.
The XML elements that a program needs can be defined as aliases as in the following code:
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);
}
The output:
[<location>1</location>, <temperature>23</temperature>, <weight>78</weight>]
Value template parameters can have default values as well. For example, the following struct template represents points in a multi-dimensional space where the default number of dimensions is 3:
值模板参数也可以有默认值。
struct Point(T, size_t dimension = 3) {
T[dimension] coordinates;
}
That template can be used without specifying the dimension template parameter:
Point!double center; // a point in 3-dimensional space
The number of dimensions can still be specified when needed:
Point!(int, 2) point; // a point on a surface
We have seen in the Variable Number of Parameters chapter how special keywords work differently depending on whether they appear inside code or as default function arguments.
Similarly, when used as default template arguments, the special keywords refer to where the template is instantiated at, not where the keywords appear:
类似地,当作为默认模板参数使用时,特殊关键字指的是模板实例化的位置,而不是关键字出现的位置。
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); // ← line 12
}
Although the special keywords appear in the definition of the template, their values refer to main(), where the template is instantiated at:
Instantiated at function deneme.main at file deneme.d, line 12.
We will use __FUNCTION__ below in a multi-dimensional operator overloading example.
this template parameters for member functions
Member functions can be templates as well. Their template parameters have the same meanings as other templates.
成员函数也可以是模板。它们的模板参数与其他模板具有相同的含义。
However, unlike other templates, member function template parameters can also be this parameters. In that case, the identifier that comes after the this keyword represents the exact type of the this reference of the object. (this reference means the object itself, as is commonly written in constructors as this.member = value.)
然而,与其他模板不同的是,成员函数模板参数也可以是
this形参。在这种情况下,this关键字后面的标识符表示对象的this引用的确切类型。this引用指的是对象本身。(在构造函数中通常写成this.member = value)
struct MyStruct(T) {
void foo(this OwnType)() const {
writeln("Type of this object: ", OwnType.stringof);
}
}
The OwnType template parameter is the actual type of the object that the member function is called on:
auto m = MyStruct!int();
auto c = const(MyStruct!int)();
auto i = immutable(MyStruct!int)();
m.foo();
c.foo();
i.foo();
The output:
Type of this object: MyStruct!int
Type of this object: const(MyStruct!int)
Type of this object: immutable(MyStruct!int)
As you can see, the type includes the corresponding type of T as well as the type qualifiers like const and immutable.
The struct (or class) need not be a template. this template parameters can appear on member function templates of non-templated types as well.
结构(或类)不需要是模板。该模板参数也可以出现在非模板类型的成员函数模板上。
this template parameters can be useful in template mixins as well, which we will see two chapters later.
alias template parameters
alias template parameters can correspond to any symbol or expression that is used in the program. The only constraint on such a template argument is that the argument must be compatible with its use inside the template.
alias模板参数可以对应于程序中使用的任何符号或表达式。对这种模板实参的唯一约束是实参必须与它在模板中的使用兼容。
filter() and map() use alias template parameters to determine the operations that they execute.
Let's see a simple example on a struct template that is for modifying an existing variable. The struct template takes the variable as an alias parameter:
struct MyStruct(alias variable) {
void set(int value) {
variable = value;
}
}
The member function simply assigns its parameter to the variable that the struct template is instantiated with. That variable must be specified during the instantiation of the template:
int x = 1;
int y = 2;
auto object = MyStruct!x();
object.set(10);
writeln("x: ", x, ", y: ", y);
In that instantiation, the variable template parameter corresponds to the variable x:
x: 10, y: 2
Conversely, MyStruct!y instantiation of the template would associate variable with y.
Let's now have an alias parameter that represents a callable entity, similar to filter() and map():
void caller(alias func)() {
write("calling: ");
func();
}
As seen by the () parentheses, caller() uses its template parameter as a function. Additionally, since the parentheses are empty, it must be legal to call the function without specifying any arguments.
Let's have the following two functions that match that description. They can both represent func because they can be called as func() in the template:
void foo() {
writeln("foo called.");
}
void bar() {
writeln("bar called.");
}
Those functions can be used as the alias parameter of caller():
caller!foo();
caller!bar();
The output:
calling: foo called.
calling: bar called.
As long as it matches the way it is used in the template, any symbol can be used as an alias parameter. As a counter example, using an int variable with caller() would cause a compilation error:
int variable;
caller!variable(); // ← compilation ERROR
The compilation error indicates that the variable does not match its use in the template:
Error: function expected before (), not variable of type int
Although the mistake is with the caller!variable instantiation, the compilation error necessarily points at func() inside the caller() template because from the point of view of the compiler the error is with trying to call variable as a function. One way of dealing with this issue is to use template constraints, which we will see below.
If the variable supports the function call syntax perhaps because it has an opCall() overload or it is a function literal, it would still work with the caller() template. The following example demonstrates both of those cases:
class C {
void opCall() {
writeln("C.opCall called.");
}
}
// ...
auto o = new C();
caller!o();
caller!({ writeln("Function literal called."); })();
The output:
calling: C.opCall called.
calling: Function literal called.
alias parameters can be specialized as well. However, they have a different specialization syntax. The specialized type must be specified between the alias keyword and the name of the parameter:
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();
}
Also note that alias parameters make the names of the actual variables available inside the template:
The general definition is using 'name' of type string.
The int specialization is using 'count'.
The double specialization is using 'length'.
Tuple template parameters
We have seen in the Variable Number of Parameters chapter that variadic functions can take any number and any type of parameters. For example, writeln() can be called with any number of parameters of any type.
Templates can be variadic as well. A template parameter that consists of a name followed by ... allows any number and kind of parameters at that parameter's position. Such parameters appear as a tuple inside the template, which can be used like an AliasSeq.
模板也可以是可变的。模板参数由一个名称后面跟着
...允许在该参数的位置上设置任意数量和类型的参数。这些参数在模板中以元组的形式出现,可以像AliasSeq一样使用。
Let's see an example of this with a template that simply prints information about every template argument that it is instantiated with:
void info(T...)(T args) {
// ...
}
The template parameter T... makes info a variadic template. Both T and args are tuples:
Trepresents the types of the arguments.argsrepresents the arguments themselves.
The following example instantiates that function template with three values of three different types:
import std.stdio;
// ...
void main() {
info(1, "abc", 2.3);
}
The following implementation simply prints information about the arguments by iterating over them in a foreach loop:
void info(T...)(T args) {
// 'args' is being used like a tuple:
foreach (i, arg; args) {
writefln("%s: %s argument %s",
i, typeof(arg).stringof, arg);
}
}
Note: As seen in the previous chapter, since the arguments are a tuple, the foreach statement above is a compile-time foreach.
正如在前一章中看到的,因为参数是一个元组,所以上面的
foreach语句是一个编译时的foreach语句。
The output:
0: int argument 1
1: string argument abc
2: double argument 2.3
Note that instead of obtaining the type of each argument by typeof(arg), we could have used T[i] as well.
We know that template arguments can be deduced for function templates. That's why the compiler deduces the types as int, string, and double in the previous program.
However, it is also possible to specify template parameters explicitly. For example, std.conv.to takes the destination type as an explicit template parameter:
to!string(42);
When template parameters are explicitly specified, they can be a mixture of value, type, and other kinds. That flexibility makes it necessary to be able to determine whether each template parameter is a type or not, so that the body of the template can be coded accordingly. That is achieved by treating the arguments as an AliasSeq.
当显式指定模板参数时,它们可以是值、类型和其他类型的混合。这种灵活性使得有必要确定每个模板参数是否为类型,以便可以相应地对模板的主体进行编码。这是通过将参数视为
AliasSeq来实现的。
Let's see an example of this in a function template that produces struct definitions as source code in text form. Let's have this function return the produced source code as string. This function can first take the name of the struct followed by the types and names of the members specified as pairs:
import std.stdio;
void main() {
writeln(structDefinition!(
"Student",
string, "name",
int, "id",
int[], "grades"
)());
}
That structDefinition instantiation is expected to produce the following string:
struct Student {
string name;
int id;
int[] grades;
}
Note: Functions that produce source code are used with the mixin keyword, which we will see in a later chapter.
The following is an implementation that produces the desired output. Note how the function template makes use of the is expression. Remember that the expression is (arg) produces true when arg is a valid type:
import std.string;
string structDefinition(string name, Members...)() {
/* Ensure that members are specified as pairs: first the
* type then the name. */
static assert((Members.length % 2) == 0,
"Members must be specified as pairs.");
/* The first part of the struct definition. */
string result = "struct " ~ name ~ "\n{\n";
foreach (i, arg; Members) {
static if (i % 2) {
/* The odd numbered arguments should be the names
* of members. Instead of dealing with the names
* here, we use them as Members[i+1] in the 'else'
* clause below.
*
* Let's at least ensure that the member name is
* specified as a string. */
static assert(is (typeof(arg) == string),
"Member name " ~ arg.stringof ~
" is not a string.");
} else {
/* In this case 'arg' is the type of the
* member. Ensure that it is indeed a type. */
static assert(is (arg),
arg.stringof ~ " is not a type.");
/* Produce the member definition from its type and
* its name.
*
* Note: We could have written 'arg' below instead
* of Members[i]. */
result ~= format(" %s %s;\n",
Members[i].stringof, Members[i+1]);
}
}
/* The closing bracket of the struct definition. */
result ~= "}";
return result;
}
import std.stdio;
void main() {
writeln(structDefinition!("Student",
string, "name",
int, "id",
int[], "grades")());
}
78.4 typeof(this), typeof(super), and typeof(return)
In some cases, the generic nature of templates makes it difficult to know or spell out certain types in the template code. The following three special typeof varieties are useful in such cases. Although they are introduced in this chapter, they work in non-templated code as well.
在某些情况下,模板的泛型特性使得在模板代码中很难知道或拼写出某些类型。下面三个特殊的
typeof变种在这种情况下很有用。虽然在本章中介绍了它们,但它们也可以在非模板代码中工作。
typeof(this)generates the type of thethisreference. It works in any struct or class, even outside of member functions:
typeof(this)生成this引用的类型。它可以在任何结构体或类中工作,甚至在成员函数之外。
struct List(T) {
// The type of 'next' is List!int when T is int
typeof(this) *next;
// ...
}
typeof(super)generates the base type of a class (i.e. the type of super).
typeof(super)生成类的基类型(也就是super的类型)。
class ListImpl(T) {
// ...
}
class List(T) : ListImpl!T {
// The type of 'next' is ListImpl!int when T is int
typeof(super) *next;
// ...
}
typeof(return)generates the return type of a function, inside that function.
typeof(return)生成函数内部的返回类型。
For example, instead of defining the calculate() function above as an auto function, we can be more explicit by replacing auto with LargerOf!(A, B) in its definition. (Being more explicit would have the added benefit of obviating at least some part of its function comment.)
LargerOf!(A, B) calculate(A, B)(A a, B b) {
// ...
}
typeof(return) prevents having to repeat the return type inside the function body:
LargerOf!(A, B) calculate(A, B)(A a, B b) {
typeof(return) result; // The type is either A or B
// ...
return result;
}
78.5 Template specializations
We have seen template specializations in the Templates chapter. Like type parameters, other kinds of template parameters can be specialized as well. The following is the general definition of a template and its specialization for 0:
void foo(int value)() {
// ... general definition ...
}
void foo(int value : 0)() {
// ... special definition for zero ...
}
We will take advantage of template specializations in the meta programming section below.
78.6 Meta programming
As they are about code generation, templates are among the higher level features of D. A template is indeed code that generates code. Writing code that generates code is called meta programming.
Due to templates being compile-time features, some operations that are normally executed at runtime can be moved to compile time as template instantiations.
由于模板是编译时特性,一些通常在运行时执行的操作可以作为模板实例化移动到编译时。
Note: Compile time function execution (CTFE) is another feature that achieves the same goal. We will see CTFE in a later chapter.
Executing templates at compile time is commonly based on recursive template instantiations.
在编译时执行模板通常基于递归模板实例化。
To see an example of this, let's first consider a regular function that calculates the sum of numbers from 0 to a specific value. For example, when its argument is 4, this fuction should return the result of 0+1+2+3+4:
int sum(int last) {
int result = 0;
foreach (value; 0 .. last + 1) {
result += value;
}
return result;
}
That is an iterative implementation of the function. The same function can be implemented by recursion as well:
int sum(int last) {
return (last == 0
? last
: last + sum(last - 1));
}
The recursive function returns the sum of the last value and the previous sum. As you can see, the function terminates the recursion by treating the value 0 specially.
Functions are normally run-time features. As usual, sum() can be executed at run time:
函数通常是运行时特性。和往常一样,
sum()可以在运行时执行。
writeln(sum(4));
When the result is needed at compile time, one way of achieving the same calculation is by defining a function template. In this case, the parameter must be a template parameter, not a function parameter:
当在编译时需要结果时,实现相同计算的一种方法是定义函数模板。在这种情况下,形参必须是模板形参,而不是函数形参。
// WARNING: This code is incorrect.
int sum(int last)() {
return (last == 0
? last
: last + sum!(last - 1)());
}
That function template instantiates itself by last - 1 and tries to calculate the sum again by recursion. However, that code is incorrect.
As the ternary operator would be compiled to be executed at run time, there is no condition check that terminates the recursion at compile time:
由于三元运算符将被编译为在运行时执行,因此在编译时不存在终止递归的条件检查。
writeln(sum!4()); // ← compilation ERROR
The compiler detects that the template instances would recurse infinitely and stops at an arbitrary number of recursions:
Error: template instance deneme.sum!(-296) recursive expansion
Considering the difference between the template argument 4 and -296, the compiler restricts template expansion at 300 by default.
In meta programming, recursion is terminated by a template specialization. The following specialization for 0 produces the expected result:
在元编程中,递归由模板专门化终止。下面对0的专门化将产生预期的结果。
// The general definition
int sum(int last)() {
return last + sum!(last - 1)();
}
// The special definition for zero
int sum(int last : 0)() {
return 0;
}
The following is a program that tests sum():
import std.stdio;
void main() {
writeln(sum!4());
}
Now the program compiles successfully and produces the result of 4+3+2+1+0:
10
An important point to make here is that the function sum!4() is executed entirely at compile time. The compiled code is the equivalent of calling writeln() with literal 10:
writeln(10); // the equivalent of writeln(sum!4())
As a result, the compiled code is as fast and simple as can be. Although the value 10 is still calculated as the result of 4+3+2+1+0, the entire calculation happens at compile time.
The previous example demonstrates one of the benefits of meta programming: moving operations from run time to compile time. CTFE obviates some of the idioms of meta programming in D.
78.7 Compile-time polymorphism
In object oriented programming (OOP), polymorphism is achieved by inheritance. For example, if a function takes an interface, it accepts objects of any class that inherits that interface.
Let's recall an earlier example from a previous chapter:
import std.stdio;
interface SoundEmitter {
string emitSound();
}
class Violin : SoundEmitter {
string emitSound() {
return "♩♪♪";
}
}
class Bell : SoundEmitter {
string emitSound() {
return "ding";
}
}
void useSoundEmittingObject(SoundEmitter object) {
// ... some operations ...
writeln(object.emitSound());
// ... more operations ...
}
void main() {
useSoundEmittingObject(new Violin);
useSoundEmittingObject(new Bell);
}
useSoundEmittingObject() is benefiting from polymorphism. It takes a SoundEmitter so that it can be used with any type that is derived from that interface.
Since working with any type is inherent to templates, they can be seen as providing a kind of polymorphism as well. Being a compile-time feature, the polymorphism that templates provide is called compile-time polymorphism. Conversely, OOP's polymorphism is called run-time polymorphism.
由于使用任何类型都是模板所固有的,因此它们也可以被视为提供了一种多态性。作为一种编译时特性,模板提供的多态性称为编译时多态性。相反,OOP的多态称为运行时多态。
In reality, neither kind of polymorphism allows being used with any type because the types must satisfy certain requirements.
Run-time polymorphism requires that the type implements a certain interface.
运行时多态性要求类型实现特定的接口。
Compile-time polymorphism requires that the type is compatible with how it is used by the template. As long as the code compiles, the template argument can be used with that template. (Note: Optionally, the argument must satisfy template constraints as well. We will see template constraints later below.)
编译时多态性要求类型与模板使用它的方式兼容。只要代码已编译,模板参数就可以与该模板一起使用。(注意:可选的是,参数也必须满足模板约束)
For example, if useSoundEmittingObject() were implemented as a function template instead of a function, it could be used with any type that supported the object.emitSound() call:
void useSoundEmittingObject(T)(T object) {
// ... some operations ...
writeln(object.emitSound());
// ... more operations ...
}
class Car {
string emitSound() {
return "honk honk";
}
}
// ...
useSoundEmittingObject(new Violin);
useSoundEmittingObject(new Bell);
useSoundEmittingObject(new Car);
Note that although Car has no inheritance relationship with any other type, the code compiles successfully, and the emitSound() member function of each type gets called.
Compile-time polymorphism is also known as duck typing, a humorous term, emphasizing behavior over actual type.
编译时多态性也被称为鸭子类型,这是一个幽默的术语,强调行为而不是实际类型。
78.8 Code bloat
The code generated by the compiler is different for every different argument of a type parameter, of a value parameter, etc.
The reason for that can be seen by considering int and double as type template arguments. Each type would have to be processed by different kinds of CPU registers. For that reason, the same template needs to be compiled differently for different template arguments. In other words, the compiler needs to generate different code for each instantiation of a template.
每种类型都必须由不同类型的CPU寄存器进行处理。因此,同一个模板需要针对不同的模板参数进行不同的编译。换句话说,编译器需要为模板的每个实例化生成不同的代码。
For example, if useSoundEmittingObject() were implemented as a template, it would be compiled as many times as the number of different instantiations of it.
Because it results in larger program size, this effect is called code bloat. Although this is not a problem in most programs, it is an effect of templates that must be known.
因为它会导致更大的程序大小,所以这种效果称为代码膨胀。虽然这在大多数程序中不是问题,但这是必须知道的模板的影响。
Conversely, non-templated version of useSoundEmittingObject() would not have any code repetition. The compiler would compile that function just once and execute the same code for all types of the SoundEmitter interface. In run-time polymorphism, having the same code behave differently for different types is achieved by function pointers on the background. Although function pointers have a small cost at run time, that cost is not significant in most programs.
相反,非模板版本不会有任何代码重复。编译器只编译该函数一次,并为所有类型的接口执行相同的代码。在运行时多态中,相同的代码对于不同的类型具有不同的行为是通过后台的函数指针实现的。尽管函数指针在运行时的开销很小,但在大多数程序中开销并不大。
Since both code bloat and run-time polymorphism have effects on program performance, it cannot be known beforehand whether run-time polymorphism or compile-time polymorphism would be a better approach for a specific program.
78.9 Template constraints
The fact that templates can be instantiated with any argument yet not every argument is compatible with every template brings an inconvenience. If a template argument is not compatible with a particular template, the incompatibility is necessarily detected during the compilation of the template code for that argument. As a result, the compilation error points at a line inside the template implementation.
Let's see this by using useSoundEmittingObject() with a type that does not support the object.emitSound() call:
class Cup {
// ... does not have emitSound() ...
}
// ...
useSoundEmittingObject(new Cup); // ← incompatible type
Although arguably the error is with the code that uses the template with an incompatible type, the compilation error points at a line inside the template:
void useSoundEmittingObject(T)(T object) {
// ... some operations ...
writeln(object.emitSound()); // ← compilation ERROR
// ... more operations ...
}
An undesired consequence is that when the template is a part of a third-party library module, the compilation error would appear to be a problem with the library itself.
Note that this issue does not exist for interfaces: A function that takes an interface can only be called with a type that implements that interface. Attempting to call such a function with any other type is a compilation error at the caller.
Template contraints are for disallowing incorrect instantiations of templates. They are defined as logical expressions of an if condition right before the template body:
void foo(T)()
if (/* ... constraints ... */) {
// ...
}
A template definition is considered by the compiler only if its constraints evaluate to true for a specific instantiation of the template. Otherwise, the template definition is ignored for that use.
Since templates are a compile-time feature, template constraints must be evaluable at compile time. The is expression that we saw in the is Expression chapter is commonly used in template constraints. We will use the is expression in the following examples as well.
因为模板是一个编译时特性,所以模板约束必须在编译时可计算。我们在is expression一章看到的
is表达式通常用于模板约束。
Tuple parameter of single element
Sometimes the single parameter of a template needs to be one of type, value, or alias kinds. That can be achieved by a tuple parameter of length one:
template myTemplate(T...)
if (T.length == 1) {
static if (is (T[0])) {
// The single parameter is a type
enum bool myTemplate = /* ... */;
} else {
// The single parameter is some other kind
enum bool myTemplate = /* ... */;
}
}
Some of the templates of the std.traits module take advantage of this idiom. We will see std.traits in a later chapter.
Named constraints
Sometimes the constraints are complex, making it hard to understand the requirements of template parameters. This complexity can be handled by an idiom that effectively gives names to constraints. This idiom combines four features of D: anonymous functions, typeof, the is expression, and eponymous templates.
Let's see this on a function template that has a type parameter. The template uses its function parameter in specific ways:
void use(T)(T object) {
// ...
object.prepare();
// ...
object.fly(42);
// ...
object.land();
// ...
}
As is obvious from the implementation of the template, the types that this function can work with must support three specific function calls on the object: prepare(), fly(42), and land().
One way of specifying a template constraint for that type is by the is and typeof expressions for each function call inside the template:
void use(T)(T object)
if (is (typeof(object.prepare())) &&
is (typeof(object.fly(1))) &&
is (typeof(object.land()))) {
// ...
}
I will explain that syntax below. For now, accept the whole construct of is (typeof(object.prepare())) to mean whether the type supports the .prepare() call.
Although such constraints achieve the desired goal, sometimes they are too complex to be readable. Instead, it is possible to give a more descriptive name to the whole constraint:
void use(T)(T object)
if (canFlyAndLand!T) {
// ...
}
That constraint is more readable because it is now more clear that the template is designed to work with types that can fly and land.
Such constraints are achieved by an idiom that is implemented similar to the following eponymous template:
template canFlyAndLand(T) {
enum canFlyAndLand = is (typeof(
{
T object;
object.prepare(); // should be preparable for flight
object.fly(1); // should be flyable for a certain distance
object.land(); // should be landable
}()));
}
The D features that take part in that idiom and how they interact with each other are explained below:
template canFlyAndLand(T) {
// (6) (5) (4)
enum canFlyAndLand = is (typeof(
{ // (1)
T object; // (2)
object.prepare();
object.fly(1);
object.land();
// (3)
}()));
}
- Anonymous function: We have seen anonymous functions in the Function Pointers, Delegates, and Lambdas chapter. The highlighted curly brackets above define an anonymous function.
- Function block: The function block uses the type as it is supposed to be used in the actual template. First an object of that type is defined and then that object is used in specific ways. (This code never gets executed; see below.)
- Evaluation of the function: The empty parentheses at the end of an anonymous function normally execute that function. However, since that call syntax is within a
typeof, it is never executed. - The typeof expression:
typeofproduces the type of an expression.
An important fact abouttypeofis that it never executes the expression. Rather, it produces the type of the expression if that expression would be executed:
int i = 42;
typeof(++i) j; // same as 'int j;'
assert(i == 42); // ++i has not been executed
As the previous assert proves, the expression ++i has not been executed. typeof has merely produced the type of that expression as int.
If the expression that typeof receives is not valid, typeof produces no type at all (not even void). So, if the anonymous function inside canFlyAndLand can be compiled successfully for T, typeof produces a valid type. Otherwise, it produces no type at all.
- The is expression: We have seen many different uses of the
isexpression in theisExpression chapter. Theis (Type)syntax producestrueifTypeis valid:
int i;
writeln(is (typeof(i))); // true
writeln(is (typeof(nonexistentSymbol))); // false
Although the second typeof above receives a nonexistent symbol, the compiler does not emit a compilation error. Rather, the effect is that the typeof expression does not produce any type, so the is expression produces false:
true
false
- Eponymous template: As described above, since the
canFlyAndLandtemplate contains a definition by the same name, the template instantiation is that definition itself.
In the end, use() gains a more descriptive constraint:
void use(T)(T object)
if (canFlyAndLand!T) {
// ...
}
Let's try to use that template with two types, one that satisfies the constraint and one that does not satisfy the constraint:
// A type that does match the template's operations
class ModelAirplane {
void prepare() {
}
void fly(int distance) {
}
void land() {
}
}
// A type that does not match the template's operations
class Pigeon {
void fly(int distance) {
}
}
// ...
use(new ModelAirplane); // ← compiles
use(new Pigeon); // ← compilation ERROR
Named or not, since the template has a constraint, the compilation error points at the line where the template is used rather than where it is implemented.
78.10 Using templates in multi-dimensional operator overloading
We have seen in the Operator Overloading chapter that opDollar, opIndex, and opSlice are for element indexing and slicing. When overloaded for single-dimensional collections, these operators have the following responsibilities:
opDollar: Returns the number of elements of the collection.opSlice: Returns an object that represents some or all of the elements of the collection.opIndex: Provides access to an element.
Those operator functions have templated versions as well, which have different responsibilities from the non-templated ones above. Note especially that in multi-dimensional operator overloading opIndex assumes the responsibility of opSlice.
这些操作符函数也有模板化版本,它们的职责与上面的非模板化版本不同。特别要注意的是,在多维操作符重载中,
opIndex承担了opSlice的责任。
opDollartemplate: Returns the length of a specific dimension of the collection. The dimension is determined by the template parameter:
size_t opDollar(size_t dimension)() const {
// ...
}
opSlicetemplate: Returns the range information that specifies the range of elements (e.g. the begin and end values inarray[begin..end]). The information can be returned asTuple!(size_t, size_t)or an equivalent type. The dimension that the range specifies is determined by the template parameter:
Tuple!(size_t, size_t) opSlice(size_t dimension)(size_t begin, size_t end) {
return tuple(begin, end);
}
opIndextemplate: Returns a range object that represents a part of the collection. The range of elements are determined by the template parameters:
Range opIndex(A...)(A arguments) {
// ...
}
opIndexAssignandopIndexOpAssignhave templated versions as well, which operate on a range of elements of the collection.
The user-defined types that define these operators can be used with the multi-dimensional indexing and slicing syntax:
// Assigns 42 to the elements specified by the
// indexing and slicing arguments:
m[a, b..c, $-1, d..e] = 42;
// ↑ ↑ ↑ ↑
// dimensions: 0 1 2 3
Such expressions are first converted to the ones that call the operator functions. The conversions are performed by replacing the $ characters with calls to opDollar!dimension(), and the index ranges with calls to opSlice!dimension(begin, end). The length and range information that is returned by those calls is in turn used as arguments when calling e.g. opIndexAssign. Accordingly, the expression above is executed as the following equivalent (the dimension values are highlighted):
// The equivalent of the above:
m.opIndexAssign(
42, // ← value to assign
a, // ← argument for dimension 0
m.opSlice!1(b, c), // ← argument for dimension 1
m.opDollar!2() - 1, // ← argument for dimension 2
m.opSlice!3(d, e)); // ← argument for dimension 3
Consequently, opIndexAssign determines the range of elements from the arguments.
Multi-dimensional operator overloading example
The following Matrix example demonstrates how these operators can be overloaded for a two-dimensional type.
Note that this code can be implemented in more efficient ways. For example, instead of constructing a single-element sub-matrix even when operating on a single element e.g. by m[i, j], it could apply the operation directly on that element.
Additionally, the writeln(__FUNCTION__) expressions inside the functions have nothing to do with the behavior of the code. They merely help expose the functions that get called behind the scenes for different operator usages.
Also note that the correctness of dimension values are enforced by template constraints.
import std.stdio;
import std.format;
import std.string;
/* Works as a two-dimensional int array. */
struct Matrix {
private:
int[][] rows;
/* Represents a range of rows or columns. */
struct Range {
size_t begin;
size_t end;
}
/* Returns the sub-matrix that is specified by the row and
* column ranges. */
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);
}
/* Assigns the specified value to each element of the
* matrix. */
Matrix opAssign(int value) {
writeln(__FUNCTION__);
foreach (row; rows) {
row[] = value;
}
return this;
}
/* Uses each element and a value in a binary operation
* and assigns the result back to that element. */
Matrix opOpAssign(string op)(int value) {
writeln(__FUNCTION__);
foreach (row; rows) {
mixin ("row[] " ~ op ~ "= value;");
}
return this;
}
/* Returns the length of the specified dimension. */
size_t opDollar(size_t dimension)() const
if (dimension <= 1) {
writeln(__FUNCTION__);
static if (dimension == 0) {
/* The length of dimension 0 is the length of the
* 'rows' array. */
return rows.length;
} else {
/* The length of dimension 1 is the lengths of the
* elements of 'rows'. */
return rows.length ? rows[0].length : 0;
}
}
/* Returns an object that represents the range from
* 'begin' to 'end'.
*
* Note: Although the 'dimension' template parameter is
* not used here, that information can be useful for other
* types. */
Range opSlice(size_t dimension)(size_t begin, size_t end)
if (dimension <= 1) {
writeln(__FUNCTION__);
return Range(begin, end);
}
/* Returns a sub-matrix that is defined by the
* arguments. */
Matrix opIndex(A...)(A arguments)
if (A.length <= 2) {
writeln(__FUNCTION__);
/* We start with ranges that represent the entire
* matrix so that the parameter-less use of opIndex
* means "all of the elements". */
Range[2] ranges = [ Range(0, opDollar!0),
Range(0, opDollar!1) ];
foreach (dimension, a; arguments) {
static if (is (typeof(a) == Range)) {
/* This dimension is already specified as a
* range like 'matrix[begin..end]', which can
* be used as is. */
ranges[dimension] = a;
} else static if (is (typeof(a) : size_t)) {
/* This dimension is specified as a single
* index value like 'matrix[i]', which we want
* to represent as a single-element range. */
ranges[dimension] = Range(a, a + 1);
} else {
/* We don't expect other types. */
static assert(
false, format("Invalid index type: %s",
typeof(a).stringof));
}
}
/* Return the sub-matrix that is specified by
* 'arguments'. */
return subMatrix(ranges[0], ranges[1]);
}
/* Assigns the specified value to each element of the
* sub-matrix. */
Matrix opIndexAssign(A...)(int value, A arguments)
if (A.length <= 2) {
writeln(__FUNCTION__);
Matrix subMatrix = opIndex(arguments);
return subMatrix = value;
}
/* Uses each element of the sub-matrix and a value in a
* binary operation and assigns the result back to that
* element. */
Matrix opIndexOpAssign(string op, A...)(int value,
A arguments)
if (A.length <= 2) {
writeln(__FUNCTION__);
Matrix subMatrix = opIndex(arguments);
mixin ("return subMatrix " ~ op ~ "= value;");
}
}
/* Executes the expression that is specified as a string, and
* prints the result as well as the new state of the
* matrix. */
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);
}