Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Classes

Similar to structs, class is a feature for defining new types. By this definition, classes are user defined types. Different from structs, classes provide the object oriented programming (OOP) paradigm in D. The major aspects of OOP are the following:

Encapsulation is achieved by protection attributes, which we will see in a later chapter. Inheritance is for acquiring implementations of other types. Polymorphism is for abstracting parts of programs from each other and is achieved by class interfaces.

This chapter will introduce classes at a high level, underlining the fact that they are reference types. Classes will be explained in more detail in later chapters.

Comparing with structs

In general, classes are very similar to structs. Most of the features that we have seen for structs in the following chapters apply to classes as well:

However, there are important differences between classes and structs.

Classes are reference types

The biggest difference from structs is that structs are value types and classes are reference types. The other differences outlined below are mostly due to this fact.

Class variables may be null

As it has been mentioned briefly in The null Value and the is Operator chapter, class variables can be null. In other words, class variables may not be providing access to any object. Class variables do not have values themselves; the actual class objects must be constructed by the new keyword.

As you would also remember, comparing a reference to null by the == or the != operator is an error. Instead, the comparison must be done by the is or the !is operator, accordingly:

    MyClass referencesAnObject = new MyClass;
    assert(referencesAnObject !is null);

    MyClass variable;   // does not reference an object
    assert(variable is null);

The reason is that, the == operator may need to consult the values of the members of the objects and that attempting to access the members through a potentially null variable would cause a memory access error. For that reason, class variables must always be compared by the is and !is operators.

Class variables versus class objects

Class variable and class object are separate concepts.

Class objects are constructed by the new keyword; they do not have names. The actual concept that a class type represents in a program is provided by a class object. For example, assuming that a Student class represents students by their names and grades, such information would be stored by the members of Student objects. Partly because they are anonymous, it is not possible to access class objects directly.

A class variable on the other hand is a language feature for accessing class objects. Although it may seem syntactically that operations are being performed on a class variable, the operations are actually dispatched to a class object.

Let's consider the following code that we saw previously in the Value Types and Reference Types chapter:

    auto variable1 = new MyClass;
    auto variable2 = variable1;

The new keyword constructs an anonymous class object. variable1 and variable2 above merely provide access to that anonymous object:

 (anonymous MyClass object)    variable1    variable2
 ───┬───────────────────┬───  ───┬───┬───  ───┬───┬───
    │        ...        │        │ o │        │ o │
 ───┴───────────────────┴───  ───┴─│─┴───  ───┴─│─┴───
              ▲                    │            │
              │                    │            │
              └────────────────────┴────────────┘
Copying

Copying affects only the variables, not the object.

Because classes are reference types, defining a new class variable as a copy of another makes two variables that provide access to the same object. The actual object is not copied.

Since no object gets copied, the postblit function this(this) is not available for classes.

    auto variable2 = variable1;

In the code above, variable2 is being initialized by variable1. The two variables start providing access to the same object.

When the actual object needs to be copied, the class must have a member function for that purpose. To be compatible with arrays, this function may be named dup(). This function must create and return a new class object. Let's see this on a class that has various types of members:

class Foo {
    S      o;  // assume S is a struct type
    char[] s;
    int    i;

// ...

    this(S o, const char[] s, int i) {
        this.o = o;
        this.s = s.dup;
        this.i = i;
    }

    Foo dup() const {
        return new Foo(o, s, i);
    }
}

The dup() member function makes a new object by taking advantage of the constructor of Foo and returns the new object. Note that the constructor copies the s member explicitly by the .dup property of arrays. Being value types, o and i are copied automatically.

The following code makes use of dup() to create a new object:

    auto var1 = new Foo(S(1.5), "hello", 42);
    auto var2 = var1.dup();

As a result, the objects that are associated with var1 and var2 are different.

Similarly, an immutable copy of an object can be provided by a member function appropriately named idup(). In this case, the constructor must be defined as pure as well. We will cover the pure keyword in a later chapter.

class Foo {
// ...
    this(S o, const char[] s, int i) pure {
        // ...

    }
    immutable(Foo) idup() const {
        return new immutable(Foo)(o, s, i);
    }
}

// ...

    immutable(Foo) imm = var1.idup();
Assignment

Just like copying, assignment affects only the variables.

Assigning to a class variable disassociates that variable from its current object and associates it with a new object.

If there is no other class variable that still provides access to the object that has been disassociated from, then that object is going to be destroyed some time in the future by the garbage collector.

    auto variable1 = new MyClass();
    auto variable2 = new MyClass();
    variable1 = variable2;

The assignment above makes variable1 leave its object and start providing access to variable2's object. Since there is no other variable for variable1's original object, that object will be destroyed by the garbage collector.

The behavior of assignment cannot be changed for classes. In other words, opAssign cannot be overloaded for them.

Definition

Classes are defined by the class keyword instead of the struct keyword:

class ChessPiece {
    // ...
}
Construction

As with structs, the name of the constructor is this. Unlike structs, class objects cannot be constructed by the { } syntax.

class ChessPiece {
    dchar shape;

    this(dchar shape) {
        this.shape = shape;
    }
}

Unlike structs, there is no automatic object construction where the constructor parameters are assigned to members sequentially:

class ChessPiece {
    dchar shape;
    size_t value;
}

void main() {
    auto king = new ChessPiece('♔', 100);  // ← compilation ERROR
}
Error: no constructor for ChessPiece

For that syntax to work, a constructor must be defined explicitly by the programmer.

Destruction

As with structs, the name of the destructor is ~this:

    ~this() {
        // ...
    }

However, different from structs, class destructors are not executed at the time when the lifetime of a class object ends. As we have seen above, the destructor is executed some time in the future during a garbage collection cycle. (By this distinction, class destructors should have more accurately been called finalizers).

As we will see later in the Memory Management chapter, class destructors must observe the following rules:

Violating these rules is undefined behavior. It is easy to see an example of such a problem simply by trying to allocate an object in a class destructor:

class C {
    ~this() {
        auto c = new C();    // ← WRONG: Allocates explicitly
                             //          in a class destructor
    }
}

void main() {
    auto c = new C();
}

The program is terminated with an exception:

core.exception.InvalidMemoryOperationError@(0)

It is equally wrong to allocate new memory indirectly from the garbage collector in a destructor. For example, memory used for the elements of a dynamic array is allocated by the garbage collector as well. Using an array in a way that would require allocating a new memory block for the elements is undefined behavior as well:

    ~this() {
        auto arr = [ 1 ];    // ← WRONG: Allocates indirectly
                             //          in a class destructor
    }
core.exception.InvalidMemoryOperationError@(0)
Member access

Same as structs, the members are accessed by the dot operator:

    auto king = new ChessPiece('♔');
    writeln(king.shape);

Although the syntax makes it look as if a member of the variable is being accessed, it is actually the member of the object. Class variables do not have members, the class objects do. The king variable does not have a shape member, the anonymous object does.

Note: It is usually not proper to access members directly as in the code above. When that exact syntax is desired, properties should be preferred, which will be explained in a later chapter.

Operator overloading

Other than the fact that opAssign cannot be overloaded for classes, operator overloading is the same as structs. For classes, the meaning of opAssign is always associating a class variable with a class object.

Member functions

Although member functions are defined and used the same way as structs, there is an important difference: Class member functions can be and by-default are overridable. We will see this concept later in the Inheritance chapter.

As overridable member functions have a runtime performance cost, without going into more detail, I recommend that you define all class functions that do not need to be overridden with the final keyword. You can apply this guideline blindly unless there are compilation errors:

class C {
    final int func() {    // ← Recommended
        // ...
    }
}

Another difference from structs is that some member functions are automatically inherited from the Object class. We will see in the next chapter how the definition of toString can be changed by the override keyword.

The is and !is operators

These operators operate on class variables.

is specifies whether two class variables provide access to the same class object. It returns true if the object is the same and false otherwise. !is is the opposite of is.

    auto myKing = new ChessPiece('♔');
    auto yourKing = new ChessPiece('♔');
    assert(myKing !is yourKing);

Since the objects of myKing and yourKing variables are different, the !is operator returns true. Even though the two objects are constructed by the same character '♔', they are still two separate objects.

When the variables provide access to the same object, is returns true:

    auto myKing2 = myKing;
    assert(myKing2 is myKing);

Both of the variables above provide access to the same object.

Summary