Structs
As has been mentioned several times earlier in the book, fundamental types are not suitable to represent higher-level concepts. For example, although a value of type int
is suitable to represent the hour of day, two int
variables would be more suitable together to represent a point in time: one for the hour and the other for the minute.
Structs are the feature that allow defining new types by combining already existing other types. The new type is defined by the struct
keyword. By this definition, structs are user defined types. Most of the content of this chapter is directly applicable to classes as well. Especially the concept of combining existing types to define a new type is exactly the same for them.
This chapter covers only the basic features of structs. We will see more of structs in the following chapters:
- Member Functions
- const ref Parameters and const Member Functions
- Constructor and Other Special Functions
- Operator Overloading
- Encapsulation and Protection Attributes
- Properties
- Contract Programming for Structs and Classes
- foreach with Structs and Classes
To understand how useful structs are, let's take a look at the addDuration()
function that we had defined earlier in the assert
and enforce
chapter. The following definition is from the exercise solution of that chapter:
void addDuration(int startHour, int startMinute, int durationHour, int durationMinute, out int resultHour, out int resultMinute) { resultHour = startHour + durationHour; resultMinute = startMinute + durationMinute; resultHour += resultMinute / 60; resultMinute %= 60; resultHour %= 24; }
Note: I will ignore the in
, out
, and unittest
blocks in this chapter to keep the code samples short.
Although the function above clearly takes six parameters, when the three pairs of parameters are considered, it is conceptually taking only three bits of information for the starting time, the duration, and the result.
Definition
The struct
keyword defines a new type by combining variables that are related in some way:
struct TimeOfDay { int hour; int minute; }
The code above defines a new type named TimeOfDay
, which consists of two variables named hour
and minute
. That definition allows the new TimeOfDay
type to be used in the program just like any other type. The following code demonstrates how similar its use is to an int
's:
int number; // a variable number = otherNumber; // taking the value of otherNumber TimeOfDay time; // an object time = otherTime; // taking the value of otherTime
The syntax of struct
definition is the following:
struct TypeName { // ... member variables and functions ... }
We will see member functions in later chapters.
The variables that a struct combines are called its members. According to this definition, TimeOfDay
has two members: hour
and minute
.
struct
defines a type, not a variable
There is an important distinction here: Especially after the Name Scope and Lifetimes and Fundamental Operations chapters, the curly brackets of struct
definitions may give the wrong impression that the struct members start and end their lives inside that scope. This is not true.
Member definitions are not variable definitions:
struct TimeOfDay { int hour; // ← Not a variable; will become a part of // a struct variable used in the program. int minute; // ← Not a variable; will become a part of // a struct variable used in the program. }
The definition of a struct
determines the types and the names of the members that the objects of that struct
will have. Those member variables will be constructed as parts of TimeOfDay
objects that take part in the program:
TimeOfDay bedTime; // This object contains its own hour // and minute member variables. TimeOfDay wakeUpTime; // This object contains its own hour // and minute member variables as // well. The member variables of // this object are not related to // the member variables of the // previous object.
Variables of struct
and class
types are called objects.
Coding convenience
Being able to combine the concepts of hour and minute together as a new type is a great convenience. For example, the function above can be rewritten in a more meaningful way by taking three TimeOfDay
parameters instead of the existing six int
parameters:
void addDuration(TimeOfDay start, TimeOfDay duration, out TimeOfDay result) { // ... }
Note: It is not normal to add two variables that represent two points in time. For example, it is meaningless to add the lunch time 12:00 to the breakfast time 7:30. It would make more sense to define another type, appropriately called Duration
, and to add objects of that type to TimeOfDay
objects. Despite this design flaw, I will continue using only TimeOfDay
objects in this chapter and introduce Duration
in a later chapter.
As you remember, functions return up-to a single value. That has precisely been the reason why the earlier definition of addDuration()
was taking two out
parameters: It could not return the hour and minute information as a single value.
Structs remove this limitation as well: Since multiple values can be combined as a single struct
type, functions can return an object of such a struct
, effectively returning multiple values at once. addDuration()
can now be defined as returning its result:
TimeOfDay addDuration(TimeOfDay start,
TimeOfDay duration) {
// ...
}
As a consequence, addDuration()
now becomes a function that produces a value, as opposed to being a function that has side effects. As you would remember from the Functions chapter, producing results is preferred over having side effects.
Structs can be members of other structs. For example, the following struct
has two TimeOfDay
members:
struct Meeting {
string topic;
size_t attendanceCount;
TimeOfDay start;
TimeOfDay end;
}
Meeting
can in turn be a member of another struct
. Assuming that there is also the Meal
struct:
struct DailyPlan {
Meeting projectMeeting;
Meal lunch;
Meeting budgetMeeting;
}
Accessing the members
Struct members are used like any other variable. The only difference is that the actual struct variable and a dot must be specified before the name of the member:
start.hour = 10;
The line above assigns the value 10 to the hour
member of the start
object.
Let's rewrite the addDuration()
function with what we have seen so far:
TimeOfDay addDuration(TimeOfDay start,
TimeOfDay duration) {
TimeOfDay result;
result.minute = start.minute + duration.minute;
result.hour = start.hour + duration.hour;
result.hour += result.minute / 60;
result.minute %= 60;
result.hour %= 24;
return result;
}
Notice that the names of the variables are now much shorter in this version of the function: start
, duration
, and result
. Additionally, instead of using complex names like startHour
, it is possible to access struct members through their respective struct variables as in start.hour
.
Here is a code that uses the new addDuration()
function. Given the start time and the duration, the following code calculates when a class period at a school would end:
void main() { TimeOfDay periodStart; periodStart.hour = 8; periodStart.minute = 30; TimeOfDay periodDuration; periodDuration.hour = 1; periodDuration.minute = 15; immutable periodEnd = addDuration(periodStart, periodDuration); writefln("Period end: %s:%s", periodEnd.hour, periodEnd.minute); }
The output:
Period end: 9:45
The main()
above has been written only by what we have seen so far. We will make this code even shorter and cleaner soon.
Construction
The first three lines of main()
are about constructing the periodStart
object and the next three lines are about constructing the periodDuration
object. In each three lines of code first an object is being defined and then its hour and minute values are being set.
In order for a variable to be used in a safe way, that variable must first be constructed in a consistent state. Because construction is so common, there is a special construction syntax for struct objects:
TimeOfDay periodStart = TimeOfDay(8, 30); TimeOfDay periodDuration = TimeOfDay(1, 15);
The values are automatically assigned to the members in the order that they are specified: Because hour
is defined first in the struct
, the value 8 is assigned to periodStart.hour
and 30 is assigned to periodStart.minute
.
As we have seen in the Type Conversions chapter, the construction syntax can be used for other types as well:
auto u = ubyte(42); // u is a ubyte auto i = int(u); // i is an int
Constructing objects as immutable
Being able to construct the object by specifying the values of its members at once makes it possible to define objects as immutable
:
immutable periodStart = TimeOfDay(8, 30); immutable periodDuration = TimeOfDay(1, 15);
Otherwise it would not be possible to mark an object first as immutable
and then modify its members:
immutable TimeOfDay periodStart; periodStart.hour = 8; // ← compilation ERROR periodStart.minute = 30; // ← compilation ERROR
Trailing members need not be specified
There may be fewer values specified than the number of members. In that case, the remaining members are initialized by the .init
values of their respective types.
The following program constructs Test
objects each time with one less constructor parameter. The assert
checks indicate that the unspecified members are initialized automatically by their .init
values. (The reason for needing to call isNaN()
is explained after the program):
import std.math; struct Test { char c; int i; double d; } void main() { // The initial values of all of the members are specified auto t1 = Test('a', 1, 2.3); assert(t1.c == 'a'); assert(t1.i == 1); assert(t1.d == 2.3); // Last one is missing auto t2 = Test('a', 1); assert(t2.c == 'a'); assert(t2.i == 1); assert(isNaN(t2.d)); // Last two are missing auto t3 = Test('a'); assert(t3.c == 'a'); assert(t3.i == int.init); assert(isNaN(t3.d)); // No initial value specified auto t4 = Test(); assert(t4.c == char.init); assert(t4.i == int.init); assert(isNaN(t4.d)); // The same as above Test t5; assert(t5.c == char.init); assert(t5.i == int.init); assert(isNaN(t5.d)); }
As you would remember from the Floating Point Types chapter, the initial value of double
is double.nan
. Since the .nan
value is unordered, it is meaningless to use it in equality comparisons. That is why calling std.math.isNaN
is the correct way of determining whether a value equals to .nan
.
Specifying default values for members
It is important that member variables are automatically initialized with known initial values. This prevents the program from continuing with indeterminate values. However, the .init
value of their respective types may not be suitable for every type. For example, char.init
is not even a valid value.
The initial values of the members of a struct can be specified when the struct is defined. This is useful for example to initialize floating point members by 0.0
, instead of the mostly-unusable .nan
.
The default values are specified by the assignment syntax as the members are defined:
struct Test { char c = 'A'; int i = 11; double d = 0.25; }
Please note that the syntax above is not really assignment. The code above merely determines the default values that will be used when objects of that struct are actually constructed later in the program.
For example, the following Test
object is being constructed without any specific values:
Test t; // no value is specified for the members writefln("%s,%s,%s", t.c, t.i, t.d);
All of the members are initialized by their default values:
A,11,0.25
Constructing by the {}
syntax
Struct objects can also be constructed by the following syntax:
TimeOfDay periodStart = { 8, 30 };
Similar to the earlier syntax, the specified values are assigned to the members in the order that they are specified. The trailing members get their default values.
This syntax is inherited from the C programming language:
auto periodStart = TimeOfDay(8, 30); // ← regular TimeOfDay periodEnd = { 9, 30 }; // ← C-style
This syntax allows designated initializers. Designated initializers are for specifying the member that an initialization value is associated with. It is even possible to initialize members in a different order than they are defined in the struct
:
TimeOfDay t = { minute: 42, hour: 7 };
Copying and assignment
Structs are value types. As has been described in the Value Types and Reference Types chapter, this means that every struct
object has its own value. Objects get their own values when constructed, and their values change when they are assigned new values.
auto yourLunchTime = TimeOfDay(12, 0); auto myLunchTime = yourLunchTime; // Only my lunch time becomes 12:05: myLunchTime.minute += 5; // ... your lunch time is still the same: assert(yourLunchTime.minute == 0);
During a copy, all of the members of the source object are automatically copied to the corresponding members of the destination object. Similarly, assignment involves assigning each member of the source to the corresponding member of the destination.
Struct members that are of reference types need extra attention.
Careful with members that are of reference types!
As you remember, copying or assigning variables of reference types does not change any value, it changes what object is being referenced. As a result, copying or assigning generates one more reference to the right-hand side object. The relevance of this for struct members is that, the members of two separate struct objects would start providing access to the same value.
To see an example of this, let's have a look at a struct where one of the members is a reference type. This struct is used for keeping the student number and the grades of a student:
struct Student { int number; int[] grades; }
The following code constructs a second Student
object by copying an existing one:
// Constructing the first object: auto student1 = Student(1, [ 70, 90, 85 ]); // Constructing the second student as a copy of the first // one and then changing its number: auto student2 = student1; student2.number = 2; // WARNING: The grades are now being shared by the two objects! // Changing the grades of the first student ... student1.grades[0] += 5; // ... affects the second student as well: writeln(student2.grades[0]);
When student2
is constructed, its members get the values of the members of student1
. Since int
is a value type, the second object gets its own number
value.
The two Student
objects also have individual grades
members as well. However, since slices are reference types, the actual elements that the two slices share are the same. Consequently, a change made through one of the slices is seen through the other slice.
The output of the code indicates that the grade of the second student has been increased as well:
75
For that reason, a better approach might be to construct the second object by the copies of the grades of the first one:
// The second Student is being constructed by the copies // of the grades of the first one: auto student2 = Student(2, student1.grades.dup); // Changing the grades of the first student ... student1.grades[0] += 5; // ... does not affect the grades of the second student: writeln(student2.grades[0]);
Since the grades have been copied by .dup
, this time the grades of the second student are not affected:
70
Note: It is possible to have even the reference members copied automatically. We will see how this is done later when covering struct member functions.
Struct literals
Similar to being able to use integer literal values like 10 in expressions without needing to define a variable, struct objects can be used as literals as well.
Struct literals are constructed by the object construction syntax.
TimeOfDay(8, 30) // ← struct literal value
Let's first rewrite the main()
function above with what we have learned since its last version. The variables are constructed by the construction syntax and are immutable
this time:
void main() { immutable periodStart = TimeOfDay(8, 30); immutable periodDuration = TimeOfDay(1, 15); immutable periodEnd = addDuration(periodStart, periodDuration); writefln("Period end: %s:%s", periodEnd.hour, periodEnd.minute); }
Note that periodStart
and periodDuration
need not be defined as named variables in the code above. Those are in fact temporary variables in this simple program, which are used only for calculating the periodEnd
variable. They could be passed to addDuration()
as literal values instead:
void main() { immutable periodEnd = addDuration(TimeOfDay(8, 30), TimeOfDay(1, 15)); writefln("Period end: %s:%s", periodEnd.hour, periodEnd.minute); }
static
members
Although objects mostly need individual copies of the struct's members, sometimes it makes sense for the objects of a particular struct type to share some variables. This may be necessary to maintain e.g. a general information about that struct type.
As an example, let's imagine a type that assigns a separate identifier for every object that is constructed of that type:
struct Point { // The identifier of each object size_t id; int line; int column; }
In order to be able to assign different ids to each object, a separate variable is needed to keep the next available id. It would be incremented every time a new object is created. Assume that nextId
is to be defined elsewhere and to be available in the following function:
Point makePoint(int line, int column) { size_t id = nextId; ++nextId; return Point(id, line, column); }
A decision must be made regarding where the common nextId
variable is to be defined. static
members are useful in such cases.
Such common information is defined as a static
member of the struct. Contrary to the regular members, there is a single variable of each static
member for each thread. (Note that most programs consist of a single thread that starts executing the main()
function.) That single variable is shared by all of the objects of that struct in that thread:
import std.stdio; struct Point { // The identifier of each object size_t id; int line; int column; // The id of the next object to construct static size_t nextId; } Point makePoint(int line, int column) { size_t id = Point.nextId; ++Point.nextId; return Point(id, line, column); } void main() { auto top = makePoint(7, 0); auto middle = makePoint(8, 0); auto bottom = makePoint(9, 0); writeln(top.id); writeln(middle.id); writeln(bottom.id); }
As nextId
is incremented at each object construction, each object gets a unique id:
0 1 2
Since static
members are owned by the entire type, there need not be an object to access them. As we have seen above, such objects can be accessed by the name of the type, as well as by the name of any object of that struct:
++Point.nextId; ++bottom.nextId; // would be the same as above
When a variable is needed not one per thread but one per program, then those variables must be defined as shared static
. We will see the shared
keyword in a later chapter.
static this()
for initialization and static ~this()
for finalization
Instead of explicitly assigning an initial value to nextId
above, we relied on its default initial value, zero. We could have used any other value:
static size_t nextId = 1000;
However, such initialization is possible only when the initial value is known at compile time. Further, some special code may have to be executed before a struct can be used in a thread. Such code is written in static this()
scopes.
For example, the following code reads the initial value from a file if that file exists:
import std.file; struct Point { // ... enum nextIdFile = "Point_next_id_file"; static this() { if (exists(nextIdFile)) { auto file = File(nextIdFile, "r"); file.readf(" %s", &nextId); } } }
The contents of static this()
blocks are automatically executed once per thread before the struct
type is ever used in that thread. Code that should be executed only once for the entire program (e.g. initializing shared
and immutable
variables) must be defined in shared static this()
and shared static ~this()
blocks, which will be covered in the Data Sharing Concurrency chapter.
Similarly, static ~this()
is for the final operations of a thread and shared static ~this()
is for the final operations of the entire program.
The following example complements the previous static this()
by writing the value of nextId
to the same file, effectively persisting the object ids over consecutive executions of the program:
struct Point { // ... static ~this() { auto file = File(nextIdFile, "w"); file.writeln(nextId); } }
The program would now initialize nextId
from where it was left off. For example, the following would be the output of the program's second execution:
3 4 5
Exercises
- Design a struct named
Card
to represent a playing card.This struct can have two members for the suit and the value. It may make sense to use an
enum
to represent the suit, or you can simply use the characters ♠, ♡, ♢, and ♣.An
int
or adchar
value can be used for the card value. If you decide to use anint
, the values 1, 11, 12, and 13 may represent the cards that do not have numbers (ace, jack, queen, and king).There are other design choices to make. For example, the card values can be represented by an
enum
type as well.The way objects of this struct will be constructed will depend on the choice of the types of its members. For example, if both members are
dchar
, thenCard
objects can be constructed like this:auto card = Card('♣', '2');
- Define a function named
printCard()
, which takes aCard
object as a parameter and simply prints it:struct Card { // ... please define the struct ... } void printCard(Card card) { // ... please define the function body ... } void main() { auto card = Card(/* ... */); printCard(card); }
For example, the function can print the 2 of clubs as:
♣2
The implementation of that function may depend on the choice of the types of the members.
- Define a function named
newDeck()
and have it return 52 cards of a deck as aCard
slice:Card[] newDeck() out (result) { assert(result.length == 52); } do { // ... please define the function body ... }
It should be possible to call
newDeck()
as in the following code:Card[] deck = newDeck(); foreach (card; deck) { printCard(card); write(' '); } writeln();
The output should be similar to the following with 52 distinct cards:
♠2 ♠3 ♠4 ♠5 ♠6 ♠7 ♠8 ♠9 ♠0 ♠J ♠Q ♠K ♠A ♡2 ♡3 ♡4 ♡5 ♡6 ♡7 ♡8 ♡9 ♡0 ♡J ♡Q ♡K ♡A ♢2 ♢3 ♢4 ♢5 ♢6 ♢7 ♢8 ♢9 ♢0 ♢J ♢Q ♢K ♢A ♣2 ♣3 ♣4 ♣5 ♣6 ♣7 ♣8 ♣9 ♣0 ♣J ♣Q ♣K ♣A
- Write a function that shuffles the deck. One way is to pick two cards at random by
std.random.uniform
, to swap those two cards, and to repeat this process a sufficient number of times. The function should take the number of repetition as a parameter:void shuffle(Card[] deck, int repetition) { // ... please define the function body ... }
Here is how it should be used:
Card[] deck = newDeck(); shuffle(deck, 1); foreach (card; deck) { printCard(card); write(' '); } writeln();
The function should swap cards
repetition
number of times. For example, when called by 1, the output should be similar to the following:♠2 ♠3 ♠4 ♠5 ♠6 ♠7 ♠8 ♠9 ♠0 ♠J ♠Q ♠K ♠A ♡2 ♡3 ♡4 ♡5 ♡6 ♡7 ♡8 ♣4 ♡0 ♡J ♡Q ♡K ♡A ♢2 ♢3 ♢4 ♢5 ♢6 ♢7 ♢8 ♢9 ♢0 ♢J ♢Q ♢K ♢A ♣2 ♣3 ♡9 ♣5 ♣6 ♣7 ♣8 ♣9 ♣0 ♣J ♣Q ♣K ♣A
A higher value for
repetition
should result in a more shuffled deck:shuffled(deck, 100);
The output:
♠4 ♣7 ♢9 ♢6 ♡2 ♠6 ♣6 ♢A ♣5 ♢8 ♢3 ♡Q ♢J ♣K ♣8 ♣4 ♡J ♣Q ♠Q ♠9 ♢0 ♡A ♠A ♡9 ♠7 ♡3 ♢K ♢2 ♡0 ♠J ♢7 ♡7 ♠8 ♡4 ♣J ♢4 ♣0 ♡6 ♢5 ♡5 ♡K ♠3 ♢Q ♠2 ♠5 ♣2 ♡8 ♣A ♠K ♣9 ♠0 ♣3
Note: A much better way of shuffling the deck is explained in the solutions.