If you have experience with object-oriented programming, you'll find much of D's OOP support familiar. As such, I need to reiterate my warning from before: D is not C++, Java, or C#. Familiarity can help you pick some things up more quickly, but it can also lead you to take other things for granted. This section introduces D's OOP features.
There are four levels of protection in D: public
, package
, private
, and protected
. The first three apply to classes, structs, and modules, but protected
only has meaning with classes. We'll examine it later when we talk about inheritance.
Anything declared public
in a module, whether it's in module scope or as part of a class
or struct
declaration, is visible anywhere it's imported. With the exception of import declarations, all declarations in a module, class
or struct
are implicitly public
. Save the following as $LEARNINGD/chapter03/protection1.d
:
module protection1; import std.stdio; // private, only visible in this module class MyClass { void sayHello() { writeln("Hello"); } } struct MyStruct { void sayHello() { writeln("Hello"); } }
Now create a new file in the same directory, let's call it importer1.d
, and add this:
void main() { import protection1; auto mc = new MyClass; mc.sayHello(); MyStruct ms; ms.sayHello(); }
Compile both files. Every symbol declared in protection1.d
is visible in importer.d
, so there are no errors on compilation.
Go back to protection1.d
and add this right after the module declaration:
private:
As this uses the colon syntax rather than braces, every symbol following this line—until another protection attribute is encountered—is now private to the protection
module. The import of std.stdio
was already private, so this has no effect on it. Compile both modules again. Now two compiler errors are produced reporting that MyClass
and MyStruct
are private. That's just what one would expect. Now save the following as protection2.d
in the same directory:
module protection2; import std.stdio; class MyClass { private void sayHello() { writeln("Hello"); } } struct MyStruct { void sayHello() { MyClass mc = new MyClass; mc.sayHello(); // calls private member function of MyClass } }
Then save this as importer2.d
, compile, and run:
void main() { import protection; MyStruct ms; ms.sayHello(); }
This will be a big surprise to most folks. In D, anything declared private
is private to the module. In other words, MyClass.sayHello
is accessible everywhere inside the protection
module. There's no such thing as private to the type in D. Unlike C++, D does not have the concept of a friend
function, as the same behavior arises via the private
and package
attributes.
Any symbol-declared package
is accessible only to modules in the same package or in any of its subpackages. To demonstrate, create two packages, encap
and encap.internal
, and populate them with a few modules. Filenames are commented:
// $LEARNINGD/chapter03/encap/support.d module encap.support; package void supportFunction() { import std.stdio: writeln; writeln("Providing support!"); } // $LEARNINGD/chapter03/encap/base.d module encap.base; void primaryFunction() { import encap.support; supportFunction(); } // $LEARNINGD/chapter03/encap/internal/help.d module encap.internal.help; void helperFunction() { import encap.support; supportFunction(); import std.stdio: writeln; writeln("Helping out!"); }
There are three modules: encap.base
, encap.support,
and encap.internal.help
. Each module declares one function, two of which are public
and one of which is package
. Now create $LEARNINGD/chapter03/packtest.d
, like so:
void main() { import encap.base; primaryFunction(); }
Finally, compile it all with the following command:
dmd packtest.d encap/base.d encap/support.d encap/internal/help.d
Ideally, the only function in the encap
package that should be accessible to the outside world is primaryFunction
. However, encap.internal.help.helperFunction
is public
. The default package protection does not extend accessibility to super packages, so helperFunction
has to be public
in order for it to be accessible inside encap
. This comes with the consequence that it's also accessible outside the package. Actually, something can be done about that. Go back to encap.internal.help
and change the declaration of helperFunction
to look like this:
package(encap) void helperFunction() {…}
Specifying a package name with the package
attribute makes the symbol accessible in that package and all of its subpackages. With this, helperFunction
is still accessible in encap.base
and is no longer accessible outside the encap
package hierarchy.
The preceding paragraphs use the terms accessible and accessibility quite a bit. We saw that, when the declarations of MyStruct
and MyClass
are private, instances cannot be instantiated in another module; the symbols are inaccessible. There's more to the story. Save the following as $LEARNINGD/chapter03/priv.d
:
module priv; private struct Priv { int x, y; } Priv makeAPriv(int x, int y) { return Priv(x, y); }
You might assume that public
members in a private
struct
declaration are meaningless. Test that assumption with $LEARNINGD/chapter03/privtest.d
:
import std.stdio;
import priv;
void main() {
auto priv = makeAPriv(10, 20);
writeln(priv.x);
}
Pass both files to the compiler; not only will it compile them, but running the resulting executable will print 10
, the correct value of priv.x
. The symbol may not be accessible, but the type itself is. Change the highlighted line to the following:
Priv priv = makeAPriv(10, 20);
This yields a compiler error similar to the one we saw earlier with MyStruct
and MyClass
. This is another benefit of type inference; a type can be completely hidden while only exposing its interface.
We can take this a couple of steps further. The type is still exposed in the function declaration, but, by replacing Priv
with auto,
the compiler will infer the return type. Also, since types can be declared inside a function, the declaration of Priv
can be moved out of module scope and into the function's scope. In this case, we have to use auto
to infer the return type, since the type does not exist outside the function scope and cannot be part of the declaration:
auto makeAPriv(int x, int y) { struct Priv { int x, y; } return Priv(x, y); }
With these changes to the priv
module, the original privtest.d
will still compile. The Priv
symbol is not accessible anywhere outside makeAPriv
. We can refer to Priv
as a type that shall not be named, or a Voldemort type.
Constructors and destructors can be implemented for classes, structs, and even modules. However, there are differences with each that need to be accounted for.
We've not explicitly declared any constructors in any class declarations so far, but we've still been able to construct new instances. That's because the compiler automatically adds a default constructor if no constructors are implemented. A default constructor is one that takes no arguments:
class OneCon { private int x; this(int x) { this.x = x; } }
OneCon
has an explicitly implemented constructor, so new OneCon()
will not compile. To create a new instance of OneCon
, a constructor argument must be provided:
auto oc = new OneCon(10);
Before the constructor is run, x
will be initialized by default. Since no default initialization value was specified in the declaration, it will be initialized to int.init
. After that, the constructor is executed and the parameter x
is assigned to the member x
. Note this.x
is used to specify the member variable, which is legally shadowed by the parameter of the same name. The this
reference is accessible in every non-static function of both classes and structs. To avoid the need to use this
so often in member functions, I prefer to prefix private member variable names with an underscore.
To enable default construction when another constructor is defined, a default constructor must also be provided. That would look like this:
class TwoCon { private int _x; this() { this(10); } this(int x) { _x = x; } }
The default constructor here sets x
to 10
by calling the single-argument constructor. Note that, unlike Java, D places no restrictions on where such a call can take place inside a constructor, meaning it need not be the first line. To invoke a default constructor, parentheses are optional:
auto tc1 = new TwoCon(); auto tc2 = new TwoCon;
Perhaps the biggest issue for C++ programmers in D is the class destructor:
class Decon { this() { writeln("Constructed!"); } this() { writeln("Destroyed!"); } void print() { writeln("Printing."); } } void printDecon() { auto d = new Decon; d.print(); } void main() { printDecon(); writeln("Leaving main."); } static ~this() { writeln("Module destructor."); }
Running this shows that the message in the class destructor is printed after the one in the static module destructor (more on that soon). Remember, the new
expression allocates memory from the GC heap. That means the garbage collector is managing the memory behind the Decon
instance. The GC will call the destructor when the memory is released, but not before. The language does not specify when the memory is released. In fact, the language says there is absolutely no guarantee that a class destructor will ever be executed. Let me repeat: you cannot depend on a class destructor ever being run. It's better to think of a class destructor in D as a closer relative of the Java finalizer than the C++ destructor. Attempting to use them for C++ style RAII is going to be painful.
In practice, the GC implementation that is used as I write runs in two scenarios: when new memory is allocated, and during the shutdown phase of DRuntime. In the previous example, the GC is never run once the Decon
instance is allocated until the app terminates. The current implementation of DRuntime happens to run static destructors before terminating the GC, but that may not always be true. Nor is there any guarantee that the GC will call any destructors during termination.
Replacing the single call to printDecon
with a loop that calls it ten times prints ten destructor messages on termination. The GC will only release memory, and therefore call destructors, as needed, and it never needs to during runtime in this case. Call it a thousand times in a loop and the GC will need to do some work; some of the destructors are called inside printDecon
, with others called at termination.
One more potential problem spot is that class destructors in D are nondeterministic, meaning that their order of execution is unpredictable. The GC can call destructors in any order. A direct consequence of this is that manipulating GC-managed memory inside a class destructor will inevitably cause an abrupt termination of the program:
class Innocent { void bye() { writeln("Bye!"); } } class Boom { Innocent _inno; this() { _inno = new Innocent(); } ~this() { _inno.bye(); } }
It's quite easy to believe that because _inno
is a member of Boom
, it will always be valid when the destructor is run. That's just not the case. From the garbage collector's perspective, the only notable fact about _inno
being a member of Boom
is that, if any given Boom
instance is no longer accessible (that is, it's eligible for collection), then _inno
is also no longer accessible. As long as no other references to that instance of Innocent
exist, it can be collected at any time. It is quite possible for the instance behind _inno
to be destroyed before the destructor is run on the Boom
instance. In that case, you'll see a segfault when _inno.bye()
is eventually called in the destructor. Never access GC-managed memory inside a class destructor.
A struct
instance can be explicitly initialized using struct literals or C-style initializers:
auto ms1 = MyStruct(10, 11);// struct literal MyStruct ms2 = {10, 11}; // C-style, not preferred MyStruct ms3 = {b:11, a:10};// Named initializers
In the first two lines, the members are initialized in the order they were declared, so 10
goes to a
and 11
goes to b
. If there are fewer initializers than members, the remaining members will be default-initialized using the .init
value for the relevant type. If there are more initializers than members, a compiler error results.
Struct literals are convenient for simple types (though they can become an annoyance if the type declaration changes), but they only allow for direct initialization of member variables. If more complex initialization is required, struct constructors should be used. A struct
does not have a default constructor. It doesn't have one generated for it, nor can one be explicitly implemented. Default construction for a struct is the same as setting it to its .init
value. Look at the following declaration:
struct StructCon { int x, y; this(int val) { x = y = val; } }
This type has two publicly accessible members and a constructor that sets each member to the same value. Here are different ways to declare a StructCon
instance:
StructCon s1; // .init: x = 0, y = 0 auto s2 = StructCon(); // .init literal: x = 0, y = 0 auto s3 = StructCon(12); // constructor: x = y = 12
In the first declaration, s1
is default initialized. In the second, s2
is explicitly initialized with the default .init
value, making it equal to s1
. In the declaration of s3
; both x
and y
are assigned the value 12
. When a struct constructor is defined, struct literals can no longer be used to initialize any instances of that type.
Default initialization can be turned off completely:
struct StructCon { int x, y; this(int val) { x = y = val; } @disable this(); }
The highlighted line tells the compiler that instances of StructCon
cannot be implicitly default initialized; they must be explicitly initialized with the .init
value or through the constructor. Be aware that this has far-reaching consequences. Consider the following example:
struct Container { StructCon sc; } Container container; // Error: default construction disabled
In order for container
to be default constructed, its member sc
must be as well, but default construction is disabled for StructCon
. To fix it, initialize sc
explicitly:
StructCon sc = StructCon.init;
A struct
in D can have a special constructor called the postblit
constructor. On the surface, it looks much like a C++ copy constructor, but it isn't. When a struct
instance is copied in D, a bitwise copy, or blit, is performed. If the type has a postblit
constructor, it is invoked after the copy is complete. They can be used to fix up anything that needs fixing up after the copy. The following example is a good use case:
struct PostBlit { int[] foo; } void printPtr(PostBlit cpb) { writeln("Inside: ", cpb.foo.ptr); } void main() { auto pb = PostBlit([10,20,30]); writeln("Outside: ", pb.foo.ptr); printPtr(pb); }
When pb
is passed into printPtr
, a copy is made, meaning the array member foo
is copied as well. Recall that, when arrays are passed around, only the metadata, and not the elements, gets copied. As such, the two writeln
calls in this example print the same address. Pass pb
into a function that operates on the array and it will be subject to the same array issues discussed in the previous chapter. To prevent the original elements from being accessed, a deep copy of the array is needed. Using the postblit
constructor:
this(this) { foo = foo.dup; }
Add this to the PostBlit
declaration and two different addresses will be printed. Remember, the struct
bits have already been copied by the time the postblit
constructor is run, so duping the slice creates a deep copy of the original array. By assigning the new slice to foo
, cbp
inside printPtr
is fixed up with a completely separate array from the original.
Let's try one more thing. Replace the postblit
constructor with the following line:
@disable this(this);
Recompiling produces an error. Disabling the postblit
constructor completely prevents any instance of PostBlit
from being copied.
Disable anything
Not only constructors, but any function can be annotated with @disable
, be they free functions or members of a class
or struct
. You can even apply it to destructors, though good luck with compiling your program if you do. @disable
is most often used to prevent default construction and copying of struct
types.
Now that you've seen so much that likely appears foreign to you, it may be a relief to know that struct
destructors behave more like they do in C++:
struct Destruct { ~this() { writeln("Destructed!"); } } void doSomething() { writeln("Initializing a Destruct"); Destruct d; writeln("Leaving the function"); } void main() { doSomething(); writeln("Leaving main"); }
Compiling and running this example should demonstrate that the destructor
is run on the Destruct
instance as soon as doSomething
exits. In this case, a struct
destructor is both reliable (it will always be called) and deterministic—if you declare multiple struct
instances in any scope, they will be destroyed in the reverse order of declaration when the scope is exited.
Consider the following example:
module classvar;
class A {}
A anInstance = new A;
Adding an empty main
function to this module and compiling produces this error:
classvar.d(3): Error: variable classvar.anInstance is mutable. Only const or immutable class thread local variables are allowed, not classvar.A
As anInstance
is mutable and thread-local, we can't initialize it with a runtime value (which is exactly what we get from the new
expression). Were it declared immutable
, const
, shared
or __gshared
(more on these later), it would compile, but it can be made to compile without them if the assignment of new A
is moved to a static constructor:
A anInstance; static this() { anInstance = new A; }
When the program is executed, DRuntime will call the static constructor before main
is entered. Let's do a little experiment. Create two modules: stat1.d
and stat2.d
. Here's the implementation of stat1
:
module stat1; static this() { writeln("stat1 constructor"); }
stat2.d
should look the same (with the name changed, of course). Now create a module, let's call it statmain.d
, consisting solely of an empty main
function. Compile it like so:
dmd statmain.d stat1.d stat2.d
When you run the executable, you should see that stat1 constructor
is printed first, followed by stat2 constructor
. Now let's change the compilation order:
dmd statmain.d stat2.d stat1.d
Running this will show you that the order of the output has also been reversed.
Although neither stat1
nor stat2
was imported anywhere, their constructors are still run. In this case, the order of execution is not specified and is implementation-dependent. Now add the following line to the top of stat1.d
:
import stat2;
Compile again, using both of the above command lines. This time, you'll see that the stat2
constructor is executed first in both cases. This is because the language guarantees that the static constructors of any imported modules will be executed before the static constructor, if any, of the importing module. Since stat1
imports stat2
, then the latter's constructor is always executed before that of stat1
.
Static destructors are always executed in the reverse of the order in which the static constructors were called. Let's add destructors to both stat1
and stat2
:
static ~this() { writeln("stat1 destructor"); }
When stat1
imports stat2
, the destructor for stat1
is always run first. Remove the import and the destructor execution order is always the opposite of the constructor execution order, whatever that may be.
Static constructors and destructors can also be declared in class
or struct
scope:
class Stat { private static Stat anInstance; static this() { writeln("Stat constructor"); anInstance = new Stat; } static ~this() { writeln("Stat destructor") } }
Add this to stat1
and you'll find the previous order of execution depends on the order of declaration: multiple static constructors in a module are executed in lexical order, with destructors executed in the reverse order. The reason to use a static class
or struct
constructor is the same as that for using module constructors: to initialize variables that can't be initialized at compile time. Since private variables are visible in the entire module, a module constructor can do the job as well:
static this() { Stat.anInstance = new Stat; }
Static constructors and destructors are always executed once per thread. We'll skip the details for now but, to guarantee that a static constructor or destructor is executed only once for the lifetime of the program, use the shared
storage class:
shared static this() { }
All static constructors marked shared
are executed before those that aren't.
In D, inheritance is only available for class
types. It looks much like Java's: multiple inheritance is prohibited and there is no such thing as public or private inheritance. All classes inherit from a DRuntime class called Object
. Take this empty class declaration:
class Simpleton {}
One of the member functions in Object
is the toString
function. You can invoke it manually when you need it, but any time a class instance is passed to one of the write
or format
functions, toString
will be invoked inside it. Try the following:
module inherit; import std.stdio; class Simpleton {} void main() { writeln(new Simpleton); }
This will print inherit.Simpleton
. The Object.toString
implementation always prints the fully qualified name of the class. By default, class member functions in D are virtual, meaning that they can be overridden by subclasses. Let's make a class that overrides the toString
method to print a message:
class Babbler { override string toString() { return "De do do do. De da da da."; } }
Instantiate a Babbler
instead of a Simpleton
in the example and it prints the message from Babbler.toString
. The override
keyword must be applied to any member function that overrides a super class function. To call the super class implementation, prefix super
to the function name:
override string toString() { import std.format : format; return format("%s: %s", super.toString(), "De do do do. De da da da."); }
Structs and the Object functions
Although a struct cannot be extended and does not descend from Object, it is still possible to implement some of the Object
functions for the runtime and library to make use of. For example, if you add a toString
function to a struct
declaration, it will work with writeln
, the same as it does for classes. override
isn't used here, since there's no inheritance.
Let's add a new function to generate a message and have toString
call that instead:
protected string genMessage() { return " De do do do. De da da da."; } override string toString() { import std.format : format; return format("%s says: %s", super.toString(), genMessage()); }
Notice how genMessage
is protected
. This makes the function accessible only to subclasses of Babbler
. Let's extend Babbler
and override genMessage
:
class Elocutioner : Babbler { protected override string genMessage() { return super.genMessage() ~ " That's all I want to say to you."; } }
Now you can take an Elocutioner
and use it anywhere a Babbler
is expected, such as an argument to a function that takes a Babbler
parameter, or in an array of Babbler
. OOP programmers will know this as polymorphism:
void babble(Babbler babbler) { writeln(babbler); } void main() { babble(new Elocutioner); }
Only public
and protected
functions can be overridden. Although private
and package
functions are still accessible to subclasses declared in the same module and package respectively, they are implicitly final
. A member function explicitly declared final
, no matter its protection level, cannot be overridden by subclasses, though it can still be overloaded. Adding final
to a class declaration prevents the class from being extended.
One point that often bites new D programmers is that overriding a function hides all overloads of the overridden function in the super class. An example:
class Base { void print(int i) { writeln(i); } void print(double d) { writeln(d); } } class Sub : Base { override void print(int i) { super.print(i * 10); } } void main() { auto s = new Sub; s.print(2.0); }
This produces a compiler error saying that Sub.print
is not callable using double
. To fix it, add an alias inside Sub
:
class Sub : Base {
alias print = super.print;
override void print(int i) {
super.print(i * 10);
}
}
Note that the name of the super class, in this case Base
, can be substituted for super
.
Calling functions outside the class namespace
Imagine a class
or struct
with a member function named writeln
. Inside any other functions in the class scope (or that of its subclasses) a call to writeln
will always call the member function, and not the function in std.stdio
. To break out of the class namespace, prepend a dot to the function call: .writeln
.
D also supports abstract classes, as in the following two declarations:
abstract class Abstract1{} class Abstract2 { abstract void abstractFunc(); }
Abstract1
is explicitly declared abstract
, whereas Abstract2
is implicitly so, since it has at least one abstract member function. Neither class can be instantiated directly. Further, any class that extends Abstract2
must either provide an implementation for abstractFunc
or itself be declared abstract
:
class Subclass : Abstract2 { override void abstractFunc() { writeln("Hello"); } }
It's always possible to upcast an instance of a subclass to a super class. For example:
auto eloc = new Elocutioner; Babbler babb = eloc;
However, it's not always possible to go in the other direction, or downcast. Given an instance of Babbler
, explicitly casting it to an Elocutioner
will succeed only if the original instance was an Elocutioner
. If it was created directly as a Babbler
, or perhaps another subclass called Orator
, the cast will fail. In that case, the result is null
:
if(cast(Elocutioner)babb) { writeln("It's an elocutioner!"); }
So far, we've been talking about implementation inheritance. D also supports interface inheritance. Again, this only works for classes. As in Java, an interface in D is declared with the interface
keyword and can contain member functions that have no implementation. Any class that implements an interface must implement each function or declare them abstract
. In this case, the override
keyword is not needed. Interfaces cannot be instantiated directly:
interface Greeter { void greet(); } class EnglishGreeter : Greeter { void greet() { writeln(this); } override string toString() { return "Hello"; } }
An instance of EnglishGreeter
can be passed anywhere a Greeter
is wanted. Be careful, though:
void giveGreeting(Greeter greeter) { greeter.greet(); writeln(greeter); // Error! }
Remember that, when given a class
instance, writeln
calls its toString
function. While EnglishGreeter
does have a toString
overriding the one that it inherited from Object
, inside giveGreeting
the instance is being viewed as a Greeter
, not an EnglishGreeter
. In D, interfaces do not inherit from Object
, so there is no toString
function for the writlen
to call.
An interface
can have static
member variables and functions (the functions must have an implementation). These behave exactly as they do in a class
or a struct
. They can be accessed using the interface
name as a namespace and the functions cannot be overridden. An interface
can also have implementations of per-instance member functions, but these must be marked with final
and cannot be overridden:
interface Boomer { final string goBoom() { return "Boom!"; } } class BoomerImp : Boomer { override string toString() { return goBoom(); } }
A single class can implement multiple interfaces. Additionally, extending any class that implements any interface causes the subclass to inherit the interfaces, for example subclasses of EnglishGreeter
are also Greeter
s.
One interesting feature of D is called alias this. Although it works with classes, the biggest benefit is to be seen with struct
types, since they can't use inheritance:
struct PrintOps {
void print(double arg) { writeln(arg); }
}
struct MathOps {
PrintOps printer;
alias printer this;
double add(double a, double b) { return a + b; }
}
Notice the syntax of the highlighted line. It differs from the standard alias
syntax. This indicates that, when a function is called on MathOps
that is not part of the MathOps
declaration, the compiler should try to call it on the printer
member instead:
MathOps ops;
auto val = ops.add(1.0, 2.0); // Calls Mathops.add
ops.print(val); // Calls Printer.print
Here, the call to ops.print
in the highlighted line is the same as calling ops.printer.print
. Obviously this isn't exactly the same thing as inheritance, but it's a simple way to reuse code. It's also handy when working with template functions. As I write, the language only allows one alias this
per struct
or class
declaration, but multiple alias this
may be supported in the future.
It's possible to nest one class declaration inside another:
class Outer { private int _x; this(int x) { _x = x; } Inner createInner() { return new Inner; } override string toString() { return "Outer"; } class Inner { override string toString() { writeln(_x); return this.outer.toString ~ ".Inner"; } } }
The first highlighted line shows that an instance of an inner class can be allocated from inside an outer class. The bottom two highlighted lines demonstrate that Inner
has access to the members of Outer
. As Inner
has no member named _x
, it can access the _x
member of Outer
directly. However, both classes have a toString
method, so we have to use this.outer.toString
to call outer's implementation. This is like using super
to call a super class function from a subclass.
Since the declaration of Inner
is public, the createInner
function isn't needed to get a new Inner
instance; it can be allocated directly. An instance of Outer
is needed to do so:
auto outer = new Outer(1); auto inner1 = outer.new Inner; // OK auto inner2 = new Outer.Inner; // Error -- need Outer 'this' auto inner2 = new outer.Inner; // Error -- same as above
By prefixing the name of the outer class instance variable to new
, that instance is associated with the inner class instance.
When a nested class is declared as static
, an instance of the outer class is no longer needed. However, a static nested class no longer has an associated outer class instance, which means that there is no .outer
property. Change the declaration of Inner
to be static
and it can no longer access the _x
and toString
members of Outer
. In other words, a static inner class is just a normal class with an additional namespace:
class Outer2 { static class StaticInner {} }
To get an instance of StaticInner
:
auto inner = new Outer2.StaticInner;
Structs can also be nested, but in this case the type name of the outer struct simply becomes an additional namespace for the inner one, for example Outer.Inner oi;
. The inner struct is just like any other struct, with no .outer
property.
3.21.246.223