Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

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:

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
  1. 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 a dchar value can be used for the card value. If you decide to use an int, 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, then Card objects can be constructed like this:

        auto card = Card('♣', '2');
    
  2. Define a function named printCard(), which takes a Card 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.

  3. Define a function named newDeck() and have it return 52 cards of a deck as a Card 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
    
  4. 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.