Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Nested Functions, Structs, and Classes

Up to this point, we have been defining functions, structs, and classes in the outermost scopes (i.e. the module scope). They can be defined in inner scopes as well. Defining them in inner scopes helps with encapsulation by narrowing the visibility of their symbols, as well as creating closures that we saw in the Function Pointers, Delegates, and Lambdas chapter.

As an example, the following outerFunc() function contains definitions of a nested function, a nested struct, and a nested class:

void outerFunc(int parameter) {
    int local;

    void nestedFunc() {
        local = parameter * 2;
    }

    struct NestedStruct {
        void memberFunc() {
            local /= parameter;
        }
    }

    class NestedClass {
        void memberFunc() {
            local += parameter;
        }
    }

    // Using the nested definitions inside this scope:

    nestedFunc();

    auto s = NestedStruct();
    s.memberFunc();

    auto c = new NestedClass();
    c.memberFunc();
}

void main() {
    outerFunc(42);
}

Like any other variable, nested definitions can access symbols that are defined in their outer scopes. For example, all three of the nested definitions above are able to use the variables named parameter and local.

As usual, the names of the nested definitions are valid only in the scopes that they are defined in. For example, nestedFunc(), NestedStruct, and NestedClass are not accessible from main():

void main() {
    auto a = NestedStruct();              // ← compilation ERROR
    auto b = outerFunc.NestedStruct();    // ← compilation ERROR
}

Although their names cannot be accessed, nested definitions can still be used in other scopes. For example, many Phobos algorithms handle their tasks by nested structs that are defined inside Phobos functions.

To see an example of this, let's design a function that consumes a slice from both ends in alternating order:

import std.stdio;
import std.array;

auto alternatingEnds(T)(T[] slice) {
    bool isFromFront = true;

    struct EndAlternatingRange {
        bool empty() const {
            return slice.empty;
        }

        T front() const {
            return isFromFront ? slice.front : slice.back;
        }

        void popFront() {
            if (isFromFront) {
                slice.popFront();
                isFromFront = false;

            } else {
                slice.popBack();
                isFromFront = true;
            }
        }
    }

    return EndAlternatingRange();
}

void main() {
    auto a = alternatingEnds([ 1, 2, 3, 4, 5 ]);
    writeln(a);
}

Even though the nested struct cannot be named inside main(), it is still usable:

[1, 5, 2, 4, 3]

Note: Because their names cannot be mentioned outside of their scopes, such types are called Voldemort types due to analogy to a Harry Potter character.

Note that the nested struct that alternatingEnds() returns does not have any member variables. That struct handles its task using merely the function parameter slice and the local function variable isFromFront. The fact that the returned object can safely use those variables even after leaving the context that it was created in is due to a closure that has been created automatically. We have seen closures in the Function Pointers, Delegates, and Lambdas chapter.

static when a closure is not needed

Since they keep their contexts alive, nested definitions are more expensive than their regular counterparts. Additionally, as they must include a context pointer to determine the context that they are associated with, objects of nested definitions occupy more space as well. For example, although the following two structs have exactly the same member variables, their sizes are different:

import std.stdio;

struct ModuleStruct {
    int i;

    void memberFunc() {
    }
}

void moduleFunc() {
    struct NestedStruct {
        int i;

        void memberFunc() {
        }
    }

    writefln("OuterStruct: %s bytes, NestedStruct: %s bytes.",
             ModuleStruct.sizeof, NestedStruct.sizeof);
}

void main() {
    moduleFunc();
}

The sizes of the two structs may be different on other environments:

OuterStruct: 4 bytes, NestedStruct: 16 bytes.

However, some nested definitions are merely for keeping them as local as possible, with no need to access variables from the outer contexts. In such cases, the associated cost would be unnecessary. The static keyword removes the context pointer from nested definitions, making them equivalents of their module counterparts. As a result, static nested definitions cannot access their outer contexts:

void outerFunc(int parameter) {
    static class NestedClass {
        int i;

        this() {
            i = parameter;    // ← compilation ERROR
        }
    }
}

The context pointer of a nested class object is available as a void* through its .outer property. For example, because they are defined in the same scope, the context pointers of the following two objects are equal:

void foo() {
    class C {
    }

    auto a = new C();
    auto b = new C();

    assert(a.outer is b.outer);
}

As we will see below, for classes nested inside classes, the type of the context pointer is the type of the outer class, not void*.

Classes nested inside classes

When a class is nested inside another one, the context that the nested object is associated with is the outer object itself.

Such nested classes are constructed by the this.new syntax. When necessary, the outer object of a nested object can be accessed by this.outer:

class OuterClass {
    int outerMember;

    class NestedClass {
        int func() {
            /* A nested class can access members of the outer
             * class. */
            return outerMember * 2;
        }

        OuterClass context() {
            /* A nested class can access its outer object
             * (i.e. its context) by '.outer'. */
            return this.outer;
        }
    }

    NestedClass algorithm() {
        /* An outer class can construct a nested object by
         * '.new'. */
        return this.new NestedClass();
    }
}

void main() {
    auto outerObject = new OuterClass();

    /* A member function of an outer class is returning a
     * nested object: */
    auto nestedObject = outerObject.algorithm();

    /* The nested object gets used in the program: */
    nestedObject.func();

    /* Naturally, the context of nestedObject is the same as
     * outerObject: */
    assert(nestedObject.context() is outerObject);
}

Instead of this.new and this.outer, .new and .outer can be used on existing objects as well:

    auto var = new OuterClass();
    auto nestedObject = var.new OuterClass.NestedClass();
    auto var2 = nestedObject.outer;
Summary