100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Classes in D

How D's reference-type classes work: single inheritance, polymorphism, the Object root, and construction/destruction lifecycle.

Memory & TypesIntermediate9 min readJul 10, 2026
Analogies

Classes in D

A class in D is a reference type: instances are created with new and allocated on the garbage-collected heap, and every class variable is really a reference to that heap object rather than the object itself. Every class implicitly derives from the root class Object, either directly or through its base class chain, which is why every class instance automatically has toString, opEquals, toHash, and opCmp methods available to override. D supports single inheritance — a class can extend exactly one base class with class Dog : Animal { ... } — but a class can additionally implement any number of interfaces, giving you multiple-interface polymorphism without the diamond-inheritance complexity of multiple class inheritance. Because assignment copies the reference and not the object, Dog a = new Dog(); Dog b = a; makes a and b point at the very same heap object, so mutating a field through b is visible through a as well.

🏏

Cricket analogy: A class instance is like a physical trophy shared by an entire franchise — everyone holding a reference to 'the IPL trophy' is pointing at the same object, so if it's engraved once, every reference sees the same engraving.

Inheritance and Polymorphism

Member functions in D classes are virtual by default, meaning a call through a base-class reference dispatches to the most-derived override at runtime; you opt out of this with the final keyword when you want to forbid further overriding, and you must mark an overriding method explicitly with override so the compiler can catch typos in the method signature instead of silently creating an unrelated new method. An abstract class, declared with abstract class Shape { abstract double area(); }, cannot be instantiated directly and exists purely to define a common interface and possibly some shared implementation for its subclasses. Interfaces, declared with interface Drawable { void draw(); }, define a pure contract with no state and no implementation (D does support default method bodies in interfaces in limited cases, but the common idiom is a pure abstract contract); a class can implement several interfaces at once with class Circle : Shape, Drawable { ... }, and any Drawable-typed reference can then hold a Circle, a Square, or anything else implementing that interface.

🏏

Cricket analogy: Virtual dispatch is like calling 'play()' on a generic Bowler reference that actually resolves to a spinner's specific delivery at runtime — the base Bowler type doesn't know in advance whether it holds an off-spinner or a legspinner object.

Construction and Destruction

Class constructors use this(...) exactly like structs, and a derived class constructor must call super(...) — explicitly or implicitly, if the base has a compatible default constructor — before accessing this, since the base portion of the object must be initialized first. Destructors are declared with ~this() and, critically, are called by the garbage collector during a collection cycle whenever it determines the object is unreachable — this timing is nondeterministic, meaning you cannot rely on a class destructor running promptly, or even at all, before the program exits. When deterministic cleanup is needed — closing a file handle, releasing a lock — call the standard library function destroy(obj), which runs the destructor immediately and resets the object's fields to their .init state, after which the reference should be treated as no longer usable (though the memory itself is only reclaimed later by the GC).

🏏

Cricket analogy: A subclass constructor calling super(...) is like a franchise's youth-academy graduate first completing the board's mandatory fitness certification (the base class setup) before being allowed to register for the domestic season (the subclass-specific setup).

Identity, Equality, and Object Members

Because two class references can point at the same object, D distinguishes identity from equality: the is operator, a is b, tests whether two references point at the exact same object (or compares to null), while the == operator calls opEquals and tests for logical equality as defined by the class, which by default falls back to Object's identity-based comparison unless overridden. Every class inherits toString() from Object, which writeln and format use automatically to produce a readable representation, so overriding toString is the idiomatic way to make a custom class print meaningfully; similarly, overriding toHash() alongside opEquals is required if instances of the class will be used as associative array keys, since the two must remain consistent with each other. Casting between related class types uses cast(Derived) baseRef, which performs a runtime check and yields null if the actual object is not in fact an instance of Derived, giving you a safe way to do type-narrowing similar to a dynamic_cast in other languages.

🏏

Cricket analogy: The is operator checking reference identity is like verifying two scorecards refer to the literal same physical sheet of paper, whereas == checking opEquals is like verifying two separately printed scorecards record the same runs and wickets even though they're different sheets.

d
import std.stdio;

abstract class Shape
{
    abstract double area() const;

    override string toString() const
    {
        import std.format : format;
        return format("%s(area=%.2f)", typeid(this).name, area());
    }
}

class Circle : Shape
{
    private double radius;

    this(double radius)
    {
        this.radius = radius; // no super() needed: Shape has no explicit ctor
    }

    override double area() const
    {
        import std.math : PI;
        return PI * radius * radius;
    }
}

class Square : Shape
{
    private double side;

    this(double side) { this.side = side; }

    override double area() const
    {
        return side * side;
    }
}

void main()
{
    Shape[] shapes = [new Circle(2.0), new Square(3.0)];

    foreach (s; shapes)
        writeln(s); // virtual dispatch picks the right area() override

    Shape a = shapes[0];
    Shape b = a;
    writeln(a is b);            // true: same object

    if (auto c = cast(Circle) shapes[0])
        writeln("radius: ", c.radius);

    destroy(a); // deterministic cleanup; GC reclaims memory later
}

Every class implicitly derives from Object, which supplies default toString, opEquals, toHash, and opCmp implementations. Overriding toString is especially common since writeln and std.format automatically call it to render class instances readably.

Never rely on ~this() running promptly, or at all, for cleanup that matters — the GC decides when (or whether, near program exit) to collect an unreachable object. For deterministic cleanup of resources like file handles or locks, call destroy(obj) explicitly, or manage the resource with a struct's deterministic destructor instead.

  • Classes are reference types allocated on the GC heap with new; assignment copies the reference, not the object.
  • Every class derives from Object, gaining default toString, opEquals, toHash, and opCmp.
  • D supports single class inheritance plus multiple interface implementation; methods are virtual by default unless marked final.
  • abstract classes cannot be instantiated and exist to define shared structure and a partial or full contract for subclasses.
  • A derived class constructor must initialize the base via super(...) before touching this.
  • ~this() runs at a nondeterministic time chosen by the GC; use destroy(obj) for deterministic cleanup.
  • is tests reference identity while == calls opEquals for logical equality; cast(Derived) safely returns null on a failed downcast.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#DProgrammingStudyNotes#ClassesInD#Classes#Inheritance#Polymorphism#Construction#OOP#StudyNotes#SkillVeris