内容简介:D was not supposed to have templates.Several months before Walter Bright released the first alpha version of the DMD compiler in December 2001, he completed a draft language specification in which he wrote:The (freely available) HOPL IV paper,
D was not supposed to have templates.
Several months before Walter Bright released the first alpha version of the DMD compiler in December 2001, he completed a draft language specification in which he wrote:
Templates. A great idea in theory, but in practice leads to numbingly complex code to implement trivial things like a “next” pointer in a singly linked list.
The (freely available) HOPL IV paper, Origins of the D Programming Language , expands on this:
[Walter] initially objected to templates on the grounds that they added disproportionate complexity to the front end, they could lead to overly complex user code, and, as he had heard many C++ users complain, they had a syntax that was difficult to understand. He would later be convinced to change his mind.
It did take some convincing. As activity grew in the (then singular) D newsgroup, requests that templates be added to the language became more frequent. Eventually, Walter started mulling over how to approach templates in a way that was less confusing for both the programmer and the compiler . Then he had an epiphany: the difference between template parameters and function parameters is one of compile time vs. run time.
From this perspective, there’s no need to introduce a special template syntax (like the C++ style <T>
) when there’s already a syntax for parameter lists in the form of (T)
. So a template declaration in D could look like this:
template foo(T, U) { // template members here }
From there, the basic features fell into place and were expanded and enhanced over time .
In this article, we’re going to lay the foundation for future articles by introducing the most basic concepts and terminology of D’s template implementation. If you’ve never used templates before in any language, this may be confusing. That’s not unexpected. Even though many find D’s templates easier to understand than other implementations, the concept itself can still be confusing. You’ll find links at the end to some tutorial resources to help build a better understanding.
Template declarations
Inside a template declaration, one can nest any other valid D declaration:
template foo(T, U) { int x; T y; struct Bar { U thing; } void doSomething(T t, U u) { ... } }
In the above example, the parameters T
and U
are template type parameters
, meaning they are generic substitutes for concrete types, which might be built-in types like int
, float
, or any type the programmer might implement with a class
or struct
declaration. By declaring a template, it’s possible to, for example, write a single implementation of a function like doSomething
that can accept multiple types for the same parameters. The compiler will generate as many copies of the function as it needs to accomodate the concrete types used in each unique template instantiation.
Other kinds of parameters are supported:value parameters, alias parameters , sequence (or variadic) parameters , andthis parameters, all of which we’ll explore in future blog posts.
One name to rule them all
In practice, it’s not very common to implement templates with multiple members. By far, the most common form of template declaration is the single-member eponymous template. Consider the following:
template max(T) { T max(T a, T b) { ... } }
An eponymous template can have multiple members that share the template name, but when there is only one, D provides us with an alternate template declaration syntax. In this example, we can opt for a normal function declaration that has the template parameter list in front of the function parameter list:
T max(T)(T a, T b) { ... }
The same holds for eponymous templates that declare an aggregate type:
// Instead of the longhand template declaration... /* template MyStruct(T, U) { struct MyStruct { T t; U u; } } */ // ...just declare a struct with a type parameter list struct MyStruct(T, U) { T t; U u; }
Eponymous templates also offer a shortcut for instantiation, as we’ll see in the next section.
Instantiating templates
In relation to templates, the term instantiate means essentially the same as it does in relation to classes and structs: create an instance of something. A template instance is, essentially, a concrete implementation of the template in which the template parameters have been replaced by the arguments provided in the instantiation. For a template function, that means a new copy of the function is generated, just as if the programmer had written it. For a type declaration, a new copy of the declaration is generated, just as if the programmer had written it.
We’ll see an example, but first we need to see the syntax.
Explicit instantiations
An explicit instantiation
is a template instance created by the programmer using the template instantiation syntax. To easily disambiguate template instantiations from function calls, D requires the template instantiation operator, !
, to be present in explicit instantiations. If the template has multiple members, they can be accessed in the same manner that members of aggregates are accessed: using dot notation.
import std; template Temp(T, U) { T x; struct Pair { T t; U u; } } void main() { Temp!(int, float).x = 10; Temp!(int, float).Pair p; p.t = 4; p.u = 3.2; writeln(Temp!(int, float).x); writeln(p); }
There is one template instantiation in this example: Temp!(int, float)
. Although it appears three times, it refers to the same instance of Temp
, one in which T == int
and U == float
. The compiler will generate declarations of x
and the Pair
type as if the programmer had written the following:
int x; struct Pair { int t; float u; }
However, we can’t just refer to x
and Pair
by themselves. We might have other instantiations of the same template, like Temp!(double, long)
, or Temp(MyStruct, short)
. To avoid conflict, the template members must be accessed through a namespace unique to each instantiation. In that regard, Temp!(int, float)
is like a class or struct with static members; just as you would access a static x
member variable in a struct Foo
using the struct name, Foo.x
, you access a template member using the template name, Temp!(int, float).x
.
There is only ever one instance of the variable x
for the instantiation Temp!(int float)
, so no matter where you use it in a code base, you will always be reading and writing the same x
. Hence, the first line of main
isn’t declaring and initializing the variable x
, but is assigning to the already declared variable. Temp!(int, float).Pair
is a struct type, so that after the declaration Temp!(int, float).Pair p
, we can refer to p
by itself. Unlike x
, p
is not a member of the template. The type Pair
is
a member, so we can’t refer to it without the prefix.
Aliased instantiations
It’s possible to simplify the syntax by using
an alias
declaration
for the instantiation:
import std; template Temp(T, U) { T x; struct Pair { T t; U u; } } alias TempIF = Temp!(int, float); void main() { TempIF.x = 10; TempIF.Pair p = TempIF.Pair(4, 3.2); writeln(TempIF.x); writeln(p); }
Since we no longer need to type the template argument list, using a struct literal to initialize p
, in this case TempIF.Pair(3, 3.2)
, looks cleaner than it would with the template arguments. So I opted to use that here rather than first declare p
and then initialize its members. We can trim it down still more
using D’s auto
attribute
, but whether this is cleaner is a matter of preference:
auto p = TempIF.Pair(4, 3.2);
Instantiating eponymous templates
Not only do eponymous templates have a shorthand declaration syntax, they also allow for a shorthand instantiation syntax. Let’s take the x
out of Temp
and rename the template to Pair
. We’re left with a Pair
template that provides a declaration struct Pair
. Then we can take advantage of both the shorthand declaration and instantiation syntaxes:
import std; struct Pair(T, U) { T t; U u; } // We can instantiate Pair without the dot operator, but still use // the alias to avoid writing the argument list every time alias PairIF = Pair!(int, float); void main() { PairIF p = PairIF(4, 3.2); writeln(p); }
The shorthand instantiation syntax means we don’t have to use the dot operator to access the Pair
type.
Even shorter syntax
When a template has only one parameter, the parentheses can be dropped from the instantiation. Take
the standard library template function std.conv.to
as an example:
void main() { import std.stdio : writeln; import std.conv : to; writeln(to!(int)("42")); }
std.conv.to
is an eponymous template, so we can use the shortened instantiation syntax. But the fact that we’ve instantiated it as an argument to the writeln
function means we’ve got three pairs of parentheses in close proximity. That sort of thing can impair readability if it pops up too often. We could move it out and store it in a variable if we really care about it, but since we’ve only got one template argument, this is a good place to drop the parentheses from the template argument list.
writeln(to!int("42"));
Whether that looks better is another case where it’s up to preference, but it’s fairly idiomatic these days to drop the parentheses for a single template argument no matter where the instantiation appears.
Not done with the short stuff yet
std.conv.to
is an interesting example because it’s an eponymous template with multiple members that share the template name. That means that it must be declared using the longform syntax (as you can see in the source code
), but we can still instantiate it without the dot notation. It’s also interesting because, even though it accepts two template arguments, it is generally only instantiated with one. This is because the second template argument can be deduced by the compiler based on the function argument.
For a somewhat simpler example,
take a look at std.utf.toUTF8
:
void main() { import std.stdio : writeln; import std.utf : toUTF8; string s1 = toUTF8("This was a UTF-16 string."w); string s2 = toUTF8("This was a UTF-32 string."d); writeln(s1); writeln(s2); }
Unlike std.conv.to
, toUTF8
takes exactly one parameter. The signature of the declaration looks like this:
string toUTF8(S)(S s)
But in the example, we aren’t passing a type as a template argument. Just as the compiler was able to deduce to
’s second argument, it’s able to deduce toUTF8
’s sole argument.
toUTF8
is an eponymous function template with a template parameter S
and a function parameter s
of type S
. There are two things we can say about this: 1) the return type is independent of the template parameter and 2) the template parameter is the type of the function parameter. Because of 2), the compiler has all the information it needs from the function call itself and has no need for the template argument in the instantiation.
Take the first call to the toUTF8
function in the declaration of s1
. In long form, it would be toUTF8!(wstring)("blah"w)
. The w
at the end of the string literal indicates it is of type wstring
, with UTF-16 encoding, as opposed to string
, with UTF-8 encoding (the default for string literals). In this situation, having to specify !(wstring)
in the template instantiation is completely redundant. The compiler already knows that the argument to the function is a wstring
. It doesn’t need the programmer to tell it that. So we can drop the template instantiation operator and the template argument list, leaving what looks like a simple function call. The compiler knows that toUTF8
is a template, knows that the template is declared with one type parameter, and knows that the type should be wstring
.
Similarly, the d
suffix on the literal in the initialization of s2
indicates a UTF-32 encoded dstring
, and the compiler knows all it needs to know for that instantiation. So in this case also, we drop the template argument and make the instantiation appear as a function call.
It does seem silly to convert a wstring
or dstring
literal to a string
when we could just drop the w
and d
prefixes and have string
literals that we can directly assign to s1
and s2
. Contrived examples and all that. But the syntax the examples are demonstrating really shines when we work with variables.
wstring ws = "This is a UTF-16 string"w; string s = ws.toUTF8; writeln(s);
Take a close look at the initialization of s
. This combines the shorthand template instantiation syntax with Uniform Function Call Syntax (UFCS) and D’s shorthand function call syntax. We’ve already seen the template syntax in this post. As for the other two:
- UFCS allows using dot notation on the first argument of any function call so that it appears as if a member function is being called. By itself, it doesn’t offer much benefit aside from, some believe, readability. Generally, it’s a matter of preference. But this feature can seriously simplify the implementation of generic templates that operate on aggregate types and built-in types.
-
The parentheses in a function call can be dropped
when the function takes no arguments, so that
foo()
becomesfoo
. In this case, the function takes one argument, but we’ve taken it out of the argument list using UFCS, so the argument list is now empty and the parentheses can be dropped. (The compiler will lower this to a normal function call,toUTF8(ws)
—it’s purely programmer convenience.) When and whether to do this in the general case is a matter of preference. The big win, again, is in the simplification of template implementations: a template can be implemented to accept a typeT
with a member variablefoo
or a member functionfoo
, or a free functionfoo
for which the first argument is of typeT
.
All of this shorthand syntax is employed to great effectin D’s range API, which allows chained function calls on types that are completely hidden from the public-facing API (akaVoldemort types).
More to come
In future articles, we’ll explore the different kinds of template parameters, introduce template constraints, see inside a template instantiation, and take a look at some of the ways people combine templates with D’s powerful compile-time features in the real world. In the meantime, here are some template tutorial resources to keep you busy:
- Ali Çehreli’s Programming in D is an excellent introduction to D in general, suitable even for those with little programming experience. The two chapters on templates ( the first called ‘Templates’ and the second ‘More Templates’ ) provide a great introduction. (The online version of the book is free, but if you find it useful, please consider throwing Ali some love by buying the ebook or print version linked at the top of the TOC .)
-
More experienced programmers may find Phillipe Sigaud’s ‘D Template Tutorial’
a good read. It’s almost a decade old, but still relevant (and still free!). This tutorial goes beyond the basics into some of the more advanced template features. It includes a look at D’s compile-time features, provides a number of examples, and sports an appendix detailing
the
is
expression (a key component of template constraints). It can also serve as a great reference when reading open source D code until you learn your way around D templates.
There are other resources, though other than my book ‘Learning D’ , I’m not aware of any that provide as much detail as the above. (And my book is currently not freely available). Eventually, we’ll be able to add this blog series to the list.
Thanks to Stefan Koch for reviewing this article.
以上所述就是小编给大家介绍的《The ABC’s of Templates in D》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C++语言的设计和演化
[美] Bjarne Stroustrup / 裘宗燕 / 机械工业出版社 / 2002-1 / 48.00元
这本书是C++的设计者关于C++语言的最主要著作之一。作者综合性地论述了C++的历史和发展,C++中各种重要机制的本质意义和设计背景,这些机制的基本用途和使用方法,讨论了C++所适合的应用领域及其未来的发展前景。一起来看看 《C++语言的设计和演化》 这本书的介绍吧!