Tuples
Tuples are for combining multiple values to be used as a single object. They are implemented as a library feature by the Tuple
template from the std.typecons
module.
Tuple
makes use of AliasSeq
from the std.meta
module for some of its operations.
This chapter covers only the more common operations of tuples. For more information on tuples and templates see Philippe Sigaud's D Templates: A Tutorial.
Tuple
and tuple()
Tuples are usually constructed by the convenience function tuple()
:
import std.stdio; import std.typecons; void main() { auto t = tuple(42, "hello"); writeln(t); }
The tuple
call above constructs an object that consists of the int
value 42 and the string
value "hello"
. The output of the program includes the type of the tuple object and its members:
Tuple!(int, string)(42, "hello")
The tuple type above is the equivalent of the following pseudo struct
definition and likely have been implemented in exactly the same way:
// The equivalent of Tuple!(int, string) struct __Tuple_int_string { int __member_0; string __member_1; }
The members of a tuple are normally accessed by their index values. That syntax suggests that tuples can be seen as arrays consisting of different types of elements:
writeln(t[0]); writeln(t[1]);
The output:
42 hello
Member properties
It is possible to access members by properties if tuple
and Tuple
templates are instantiated with member names. Both of the methods below have the same effect:
auto a = tuple!("number", "message")(42, "hello"); auto b = Tuple!(int, "number", string, "message")(42, "hello");
The definitions above allow accessing the members by .number
and .message
properties as well:
writeln("by index 0 : ", a[0]); writeln("by .number : ", a.number); writeln("by index 1 : ", a[1]); writeln("by .message: ", a.message);
The output:
by index 0 : 42 by .number : 42 by index 1 : hello by .message: hello
Expanding the members as a list of values
Tuple members can be expanded as a list of values that can be used e.g. as an argument list when calling a function. The members can be expanded either by the .expand
property or by slicing:
import std.stdio; import std.typecons; void foo(int i, string s, double d, char c) { // ... } void bar(int i, double d, char c) { // ... } void main() { auto t = tuple(1, "2", 3.3, '4'); // Both of the following lines are equivalents of // foo(1, "2", 3.3, '4'): foo(t.expand); foo(t[]); // The equivalent of bar(1, 3.3, '4'): bar(t[0], t[$-2..$]); }
The tuple above consists of four values of int
, string
, double
, and char
. Since those types match the parameter list of foo()
, an expansion of its members can be used as arguments to foo()
. When calling bar()
, a matching argument list is made up of the first member and the last two members of the tuple.
As long as the members are compatible to be elements of the same array, the expansion of a tuple can be used as the element values of an array literal as well:
import std.stdio; import std.typecons; void main() { auto t = tuple(1, 2, 3); auto a = [ t.expand, t[] ]; writeln(a); }
The array literal above is initialized by expanding the same tuple twice:
[1, 2, 3, 1, 2, 3]
Compile-time foreach
Because their values can be expanded, tuples can be used with the foreach
statement as well:
auto t = tuple(42, "hello", 1.5); foreach (i, member; t) { writefln("%s: %s", i, member); }
The output:
0: 42 1: hello 2: 1.5
The foreach
statement above may give a false impression: It may be thought of being a loop that gets executed at run time. That is not the case. Rather, a foreach
statement that operates on the members of a tuple is an unrolling of the loop body for each member. The foreach
statement above is the equivalent of the following code:
{ enum size_t i = 0; int member = t[i]; writefln("%s: %s", i, member); } { enum size_t i = 1; string member = t[i]; writefln("%s: %s", i, member); } { enum size_t i = 2; double member = t[i]; writefln("%s: %s", i, member); }
The reason for the unrolling is the fact that when the tuple members are of different types, the foreach
body has to be compiled differently for each type.
We will see static foreach
, a more powerful loop unrolling feature, in a later chapter.
Returning multiple values from functions
Tuples can be a simple solution to the limitation of functions having to return a single value. An example of this is std.algorithm.findSplit
. findSplit()
searches for a range inside another range and produces a result consisting of three pieces: the part before the found range, the found range, and the part after the found range:
import std.algorithm; // ... auto entireRange = "hello"; auto searched = "ll"; auto result = findSplit(entireRange, searched); writeln("before: ", result[0]); writeln("found : ", result[1]); writeln("after : ", result[2]);
The output:
before: he found : ll after : o
Another option for returning multiple values from a function is to return a struct
object:
struct Result { // ... } Result foo() { // ... }
AliasSeq
AliasSeq
is defined in the std.meta
module. It is used for representing a concept that is normally used by the compiler but otherwise not available to the programmer as an entity: A comma-separated list of values, types, and symbols (i.e. alias
template arguments). The following are three examples of such lists:
- Function argument list
- Template argument list
- Array literal element list
The following three lines of code are examples of those lists in the same order:
foo(1, "hello", 2.5); // function arguments auto o = Bar!(char, long)(); // template arguments auto a = [ 1, 2, 3, 4 ]; // array literal elements
Tuple
takes advantage of AliasSeq
when expanding its members.
The name AliasSeq
comes from "alias sequence" and it can contain types, values, and symbols. (AliasSeq
and std.meta
used to be called TypeTuple
and std.typetuple
, respectively.)
This chapter includes AliasSeq
examples that consist only of types or only of values. Examples of its use with both types and values will appear in the next chapter. AliasSeq
is especially useful with variadic templates, which we will see in the next chapter as well.
AliasSeq
consisting of values
The values that an AliasSeq
represents are specified as its template arguments.
Let's imagine a function that takes three parameters:
import std.stdio; void foo(int i, string s, double d) { writefln("foo is called with %s, %s, and %s.", i, s, d); }
That function would normally be called with three arguments:
foo(1, "hello", 2.5);
AliasSeq
can combine those arguments as a single entity and can automatically be expanded when calling functions:
import std.meta; // ... alias arguments = AliasSeq!(1, "hello", 2.5); foo(arguments);
Although it looks like the function is now being called with a single argument, the foo()
call above is the equivalent of the previous one. As a result, both calls produce the same output:
foo is called with 1, hello, and 2.5.
Also note that arguments
is not defined as a variable, e.g. with auto
. Rather, it is an alias
of a specific AliasSeq
instance. Although it is possible to define variables of AliasSeq
s as well, the examples in this chapter will use them only as aliases.
As we have seen above with Tuple
, when the values are compatible to be elements of the same array, an AliasSeq
can be used to initialize an array literal as well:
alias elements = AliasSeq!(1, 2, 3, 4); auto arr = [ elements ]; assert(arr == [ 1, 2, 3, 4 ]);
Indexing and slicing
Same with Tuple
, the members of an AliasSeq
can be accessed by indexes and slices:
alias arguments = AliasSeq!(1, "hello", 2.5); assert(arguments[0] == 1); assert(arguments[1] == "hello"); assert(arguments[2] == 2.5);
Let's assume there is a function with parameters matching the last two members of the AliasSeq
above. That function can be called with a slice of just the last two members of the AliasSeq
:
void bar(string s, double d) { // ... } // ... bar(arguments[$-2 .. $]);
AliasSeq
consisting of types
Members of an AliasSeq
can consist of types. In other words, not a specific value of a specific type but a type like int
itself. An AliasSeq
consisting of types can represent template arguments.
Let's use an AliasSeq
with a struct
template that has two parameters. The first parameter of this template determines the element type of a member array and the second parameter determines the return value of a member function:
import std.conv; struct S(ElementT, ResultT) { ElementT[] arr; ResultT length() { return to!ResultT(arr.length); } } void main() { auto s = S!(double, int)([ 1, 2, 3 ]); auto l = s.length(); }
In the code above, we see that the template is instantiated with (double, int)
. An AliasSeq
can represent the same argument list as well:
import std.meta; // ... alias Types = AliasSeq!(double, int); auto s = S!Types([ 1, 2, 3 ]);
Although it appears to be a single template argument, Types
gets expanded automatically and the template instantiation becomes S!(double, int)
as before.
AliasSeq
is especially useful in variadic templates. We will see examples of this in the next chapter.
foreach
with AliasSeq
Same with Tuple
, the foreach
statement operating on an AliasSeq
is not a run time loop. Rather, it is the unrolling of the loop body for each member.
Let's see an example of this with a unit test written for the S
struct that was defined above. The following code tests S
for element types int
, long
, and float
(ResultT
is always size_t
in this example):
unittest { alias Types = AliasSeq!(int, long, float); foreach (Type; Types) { auto s = S!(Type, size_t)([ Type.init, Type.init ]); assert(s.length() == 2); } }
The foreach
variable Type
corresponds to int
, long
, and float
, in that order. As a result, the foreach
statement gets compiled as the equivalent of the code below:
{ auto s = S!(int, size_t)([ int.init, int.init ]); assert(s.length() == 2); } { auto s = S!(long, size_t)([ long.init, long.init ]); assert(s.length() == 2); } { auto s = S!(float, size_t)([ float.init, float.init ]); assert(s.length() == 2); }
.tupleof
property
.tupleof
represents the members of a type or an object. When applied to a user-defined type, .tupleof
provides access to the definitions of the members of that type:
import std.stdio; struct S { int number; string message; double value; } void main() { foreach (i, MemberType; typeof(S.tupleof)) { writefln("Member %s:", i); writefln(" type: %s", MemberType.stringof); string name = S.tupleof[i].stringof; writefln(" name: %s", name); } }
S.tupleof
appears in two places in the program. First, the types of the elements are obtained by applying typeof
to .tupleof
so that each type appears as the MemberType
variable. Second, the name of the member is obtained by S.tupleof[i].stringof
.
Member 0: type: int name: number Member 1: type: string name: message Member 2: type: double name: value
.tupleof
can be applied to an object as well. In that case, it produces a tuple consisting of the values of the members of the object:
auto object = S(42, "hello", 1.5); foreach (i, member; object.tupleof) { writefln("Member %s:", i); writefln(" type : %s", typeof(member).stringof); writefln(" value: %s", member); }
The foreach
variable member
represents each member of the object:
Member 0: type : int value: 42 Member 1: type : string value: hello Member 2: type : double value: 1.5
Here, an important point to make is that the tuple that .tupleof
returns consists of the members of the object themselves, not their copies. In other words, the tuple members are references to the actual object members.
Summary
tuple()
combines different types of values similar to astruct
object.- The members can be accessed by properties
- The members can be expanded as a value list by
.expand
or by slicing. foreach
with a tuple is not a run time loop; rather, it is a loop unrolling.AliasSeq
represents concepts like function argument list, template argument list, array literal element list, etc.AliasSeq
can consist of values and types.- Tuples support indexing and slicing.
.tupleof
provides information about the members of types and objects.