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
- Functions, structs, and classes that are defined in inner scopes can access those scopes as their contexts.
- Nested definitions keep their contexts alive to form closures.
- Nested definitions are more costly than their module counterparts. When a nested definition does not need to access its context, this cost can be avoided by the
static
keyword. - Classes can be nested inside other classes. The context of such a nested object is the outer object itself. Nested class objects are constructed by
this.new
orvariable.new
and their contexts are available bythis.outer
orvariable.outer
.