Constructor and Other Special Functions
Although this chapter focuses only on structs, the topics that are covered here apply mostly to classes as well. The differences will be explained in later chapters.
Four member functions of structs are special because they define the fundamental operations of that type:
this()
for construction~this()
for destructionthis(ref const(S))
for copy construction(
S
is just an example there, representing the type of the struct.)opAssign()
for assignment
In addition, there is a legacy function, which is not recommended for newly written code:
this(this)
for postblit
These fundamental operations are handled automatically for structs. But it is possible to define them manually to provide different implementations when needed.
Constructor
The responsibility of the constructor is to prepare an object for use by assigning appropriate values to its members.
We have already used constructors in previous chapters. When the name of a type is used like a function, it is actually the constructor that gets called. We can see this on the right-hand side of the following line:
auto busArrival = TimeOfDay(8, 30);
Similarly, a class object is being constructed on the right hand side of the following line:
auto variable = new SomeClass();
The arguments that are specified within parentheses correspond to the constructor parameters. For example, the values 8 and 30 above are passed to the TimeOfDay
constructor as its parameters.
In addition to different object construction syntaxes that we have seen so far; const
, immutable
, and shared
objects can be constructed with the type constructor syntax as well (e.g. as immutable(S)(2)
). (We will see the shared
keyword in a later chapter.)
For example, although all three variables below are immutable
, the construction of variable a
is semantically different from the constructions of variables b
and c
:
/* More familiar syntax; immutable variable of a mutable * type: */ immutable a = S(1); /* Type constructor syntax; a variable of an immutable * type: */ auto b = immutable(S)(2); /* Same meaning as 'b' */ immutable c = immutable(S)(3);
Constructor syntax
Different from other functions, constructors do not have return values. The name of the constructor is always this
:
struct SomeStruct { // ... this(/* constructor parameters */) { // ... operations that prepare the object for use ... } }
The constructor parameters include information that is needed to make a useful and consistent object.
Compiler-generated automatic constructor
All of the structs that we have seen so far have been taking advantage of a constructor that has been generated automatically by the compiler. The automatic constructor assigns the parameter values to the members in the order that they are specified.
As you will remember from the Structs chapter, the initial values for the trailing members need not be specified. The members that are not specified get initialized by the .init
value of their respective types. The .init
values of a member could be provided during the definition of that member after the =
operator:
struct Test { int member = 42; }
Also considering the default parameter values feature from the Variable Number of Parameters chapter, we can imagine that the automatic constructor for the following struct
would be the equivalent of the following this()
:
struct Test { char c; int i; double d; /* The equivalent of the compiler-generated automatic * constructor (Note: This is only for demonstration; the * following constructor would not actually be called * when default-constructing the object as Test().) */ this(in char c_parameter = char.init, in int i_parameter = int.init, in double d_parameter = double.init) { c = c_parameter; i = i_parameter; d = d_parameter; } }
For most structs, the compiler-generated constructor is sufficient: Providing appropriate values for each member is all that is needed for objects to be constructed.
Accessing the members by this.
To avoid mixing the parameters with the members, the parameter names above had _parameter
appended to their names. There would be compilation errors without doing that:
struct Test { char c; int i; double d; this(in char c = char.init, in int i = int.init, in double d = double.init) { // An attempt to assign an 'in' parameter to itself! c = c; // ← compilation ERROR i = i; d = d; } }
The reason is; c
alone would mean the parameter, not the member, and as the parameters above are defined as in
, they cannot be modified:
Error: variable deneme.Test.this.c cannot modify const
A solution is to prepend the member names with this.
. Inside member functions, this
means "this object", making this.c
mean "the c member of this object":
this(in char c = char.init, in int i = int.init, in double d = double.init) { this.c = c; this.i = i; this.d = d; }
Now c
alone means the parameter and this.c
means the member, and the code compiles and works as expected: The member c
gets initialized by the value of the parameter c
.
User-defined constructors
I have described the behavior of the compiler-generated constructor. Since that constructor is suitable for most cases, there is no need to define a constructor by hand.
Still, there are cases where constructing an object involves more complicated operations than assigning values to each member in order. As an example, let's consider Duration
from the earlier chapters:
struct Duration { int minute; }
The compiler-generated constructor is sufficient for this single-member struct:
time.decrement(Duration(12));
Since that constructor takes the duration in minutes, the programmers would sometimes need to make calculations:
// 23 hours and 18 minutes earlier time.decrement(Duration(23 * 60 + 18)); // 22 hours and 20 minutes later time.increment(Duration(22 * 60 + 20));
To eliminate the need for these calculations, we can design a Duration
constructor that takes two parameters and makes the calculation automatically:
struct Duration { int minute; this(int hour, int minute) { this.minute = hour * 60 + minute; } }
Since hour and minute are now separate parameters, the users simply provide their values without needing to make the calculation themselves:
// 23 hours and 18 minutes earlier time.decrement(Duration(23, 18)); // 22 hours and 20 minutes later time.increment(Duration(22, 20));
First assignment to a member is construction
When setting values of members in a constructor, the first assignment to each member is treated specially: Instead of assigning a new value over the .init
value of that member, the first assignment actually constructs that member. Further assignments to that member are treated regularly as assignment operations.
This special behavior is necessary so that immutable
and const
members can in fact be constructed with values known only at run time. Otherwise, they could never be set to desired values as assignment is disallowed for immutable
and const
variables.
The following program demonstrates how assigment operation is allowed only once for an immutable
member:
struct S { int m; immutable int i; this(int m, int i) { this.m = m; // ← construction this.m = 42; // ← assignment (possible for mutable member) this.i = i; // ← construction this.i = i; // ← compilation ERROR } } void main() { auto s = S(1, 2); }
User-defined constructor disables compiler-generated constructor
A constructor that is defined by the programmer makes some uses of the compiler-generated constructor invalid: Objects cannot be constructed by default parameter values anymore. For example, trying to construct Duration
by a single parameter is a compilation error:
time.decrement(Duration(12)); // ← compilation ERROR
The compilation error is due to the fact that the programmer's constructor does not take a single parameter and the compiler-generated constructor is disabled.
One solution is to overload the constructor by providing another constructor that takes just one parameter:
struct Duration { int minute; this(int hour, int minute) { this.minute = hour * 60 + minute; } this(int minute) { this.minute = minute; } }
A user-defined constructor disables constructing objects by the { }
syntax as well:
Duration duration = { 5 }; // ← compilation ERROR
Initializing without providing any parameter is still valid:
auto d = Duration(); // compiles
The reason is, in D, the .init
value of every type must be known at compile time. The value of d
above is equal to the initial value of Duration
:
assert(d == Duration.init);
static opCall
instead of the default constructor
Because the initial value of every type must be known at compile time, it is impossible to define the default constructor explicitly.
Let's consider the following constructor that tries to print some information every time an object of that type is constructed:
struct Test { this() { // ← compilation ERROR writeln("A Test object is being constructed."); } }
The compiler output:
Error: constructor deneme.Deneme.this default constructor for structs only allowed with @disable and no body
Note: We will see in later chapters that it is possible to define the default constructor for classes.
As a workaround, a parameterless static opCall()
can be used for constructing objects without providing any parameters. Note that this has no effect on the .init
value of the type.
For this to work, static opCall()
must construct and return an object of that struct type:
import std.stdio; struct Test { static Test opCall() { writeln("A Test object is being constructed."); Test test; return test; } } void main() { auto test = Test(); }
The Test()
call in main()
executes static opCall()
:
A Test object is being constructed.
Note that it is not possible to type Test()
inside static opCall()
. That syntax would execute static opCall()
again and cause an infinite recursion:
static Test opCall() { writeln("A Test object is being constructed."); return Test(); // ← Calls 'static opCall()' again }
The output:
A Test object is being constructed.
A Test object is being constructed.
A Test object is being constructed.
... ← repeats the same message
Calling other constructors
Constructors can call other constructors to avoid code duplication. Although Duration
is too simple to demonstrate how useful this feature is, the following single-parameter constructor takes advantage of the two-parameter constructor:
this(int hour, int minute) { this.minute = hour * 60 + minute; } this(int minute) { this(0, minute); // calls the other constructor }
The constructor that only takes the minute value calls the other constructor by passing 0 as the value of hour.
Warning: There is a design flaw in the Duration
constructors above because the intention is not clear when the objects are constructed by a single parameter:
// 10 hours or 10 minutes? auto travelDuration = Duration(10);
Although it is possible to determine by reading the documentation or the code of the struct that the parameter actually means "10 minutes," it is an inconsistency as the first parameter of the two-parameter constructor is hours.
Such design mistakes are causes of bugs and must be avoided.
Constructor qualifiers
Normally, the same constructor is used for mutable, const
, immutable
, and shared
objects:
import std.stdio; struct S { this(int i) { writeln("Constructing an object"); } } void main() { auto m = S(1); const c = S(2); immutable i = S(3); shared s = S(4); }
Semantically, the objects that are constructed on the right-hand sides of those expressions are all mutable; only the variables have different type qualifiers. The same constructor is used for all of them:
Constructing an object Constructing an object Constructing an object Constructing an object
Depending on the qualifier of the resulting object, sometimes some members may need to be initialized differently or need not be initialized at all. For example, since no member of an immutable
object can be mutated throughout the lifetime of that object, leaving its mutable members uninitialized can improve program performance.
Qualified constructors can be defined differently for objects with different qualifiers:
import std.stdio; struct S { this(int i) { writeln("Constructing an object"); } this(int i) const { writeln("Constructing a const object"); } this(int i) immutable { writeln("Constructing an immutable object"); } // We will see the 'shared' keyword in a later chapter. this(int i) shared { writeln("Constructing a shared object"); } } void main() { auto m = S(1); const c = S(2); immutable i = S(3); shared s = S(4); }
However, as indicated above, as the right-hand side expressions are all semantically mutable, those objects are still constructed with the mutable object contructor:
Constructing an object Constructing an object ← NOT the const constructor Constructing an object ← NOT the immutable constructor Constructing an object ← NOT the shared constructor
To take advantage of qualified constructors, one must use the type constructor syntax. (The term type constructor should not be confused with object constructors; type constructor is related to types, not objects.) This syntax makes a different type by combining a qualifier with an existing type. For example, immutable(S)
is a qualified type made from immutable
and S
:
auto m = S(1); auto c = const(S)(2); auto i = immutable(S)(3); auto s = shared(S)(4);
This time, the objects that are in the right-hand expressions are different: mutable, const
, immutable
, and shared
, respectively. As a result, each object is constructed with its matching constructor:
Constructing an object Constructing a const object Constructing an immutable object Constructing a shared object
As expected, since all of the variables above are defined with the auto
keyword, they are correctly inferred to be mutable, const
, immutable
, and shared
, respectively.
Immutability of constructor parameters
In the Immutability chapter we have seen that it is not easy to decide whether parameters of reference types should be defined as const
or immutable
. Although the same considerations apply for constructor parameters as well, immutable
is usually a better choice for constructor parameters.
The reason is, it is common to assign the parameters to members to be used at a later time. When a parameter is not immutable
, there is no guarantee that the original variable will not change by the time the member gets used.
Let's consider a constructor that takes a file name as a parameter. The file name will be used later on when writing student grades. According to the guidelines in the Immutability chapter, to be more useful, let's assume that the constructor parameter is defined as const char[]
:
import std.stdio; struct Student { const char[] fileName; int[] grades; this(const char[] fileName) { this.fileName = fileName; } void save() { auto file = File(fileName.idup, "w"); file.writeln("The grades of the student:"); file.writeln(grades); } // ... } void main() { char[] fileName; fileName ~= "student_grades"; auto student = Student(fileName); // ... /* Assume the fileName variable is modified later on * perhaps unintentionally (all of its characters are * being set to 'A' here): */ fileName[] = 'A'; // ... /* The grades would be written to the wrong file: */ student.save(); }
The program above saves the grades of the student under a file name that consists of A characters, not to "student_grades"
. For that reason, sometimes it is more suitable to define constructor parameters and members of reference types as immutable
. We know that this is easy for strings by using aliases like string
. The following code shows the parts of the struct that would need to be modified:
struct Student { string fileName; // ... this(string fileName) { // ... } // ... }
Now the users of the struct must provide immutable
strings and as a result the confusion about the name of the file would be prevented.
Type conversions through single-parameter constructors
Single-parameter constructors can be thought of as providing a sort of type conversion: They produce an object of the particular struct type starting from a constructor parameter. For example, the following constructor produces a Student
object from a string
:
struct Student { string name; this(string name) { this.name = name; } }
to()
and cast
observe this behavior as a conversion as well. To see examples of this, let's consider the following salute()
function. Sending a string
parameter when it expects a Student
would naturally cause a compilation error:
void salute(Student student) { writeln("Hello ", student.name); } // ... salute("Jane"); // ← compilation ERROR
On the other hand, all of the following lines ensure that a Student
object is constructed before calling the function:
import std.conv; // ... salute(Student("Jane")); salute(to!Student("Jean")); salute(cast(Student)"Jim");
to
and cast
take advantage of the single-parameter constructor by constructing a temporary Student
object and calling salute()
with that object.
Destructor
The destructor includes the operations that must be executed when the lifetime of an object ends.
The compiler-generated automatic destructor executes the destructors of all of the members in order. For that reason, as it is with the constructor, there is no need to define a destructor for most structs.
However, sometimes some special operations may need to be executed when an object's lifetime ends. For example, an operating system resource that the object owns may need to be returned to the system; a member function of another object may need to be called; a server running somewhere on the network may need to be notified that a connection to it is about to be terminated; etc.
The name of the destructor is ~this
and just like constructors, it has no return type.
Destructor is executed automatically
The destructor is executed as soon as the lifetime of the struct object ends. (This is not the case for objects that are constructed with the new
keyword.)
As we have seen in the Lifetimes and Fundamental Operations chapter, the lifetime of an object ends when leaving the scope that it is defined in. The following are times when the lifetime of a struct ends:
- When leaving the scope of the object either normally or due to a thrown exception:
if (aCondition) { auto duration = Duration(7); // ... } // ← The destructor is executed for 'duration' // at this point
- Anonymous objects are destroyed at the end of the whole expression that they are constructed in:
time.increment(Duration(5)); // ← The Duration(5) object // gets destroyed at the end // of the whole expression.
- All of the struct members of a struct object get destroyed when the outer object is destroyed.
Destructor example
Let's design a type for generating simple XML documents. XML elements are defined by angle brackets. They contain data and other XML elements. XML elements can have attributes as well; we will ignore them here.
Our aim will be to ensure that an element that has been opened by a <name>
tag will always be closed by a matching </name>
tag:
<class1> ← opening the outer XML element <grade> ← opening the inner XML element 57 ← the data </grade> ← closing the inner element </class1> ← closing the outer element
A struct that can produce the output above can be designed by two members that store the tag for the XML element and the indentation to use when printing it:
struct XmlElement {
string name;
string indentation;
}
If the responsibilities of opening and closing the XML element are given to the constructor and the destructor, respectively, the desired output can be produced by managing the lifetimes of XmlElement objects. For example, the constructor can print <tag>
and the destructor can print </tag>
.
The following definition of the constructor produces the opening tag:
this(string name, int level) { this.name = name; this.indentation = indentationString(level); writeln(indentation, '<', name, '>'); }
indentationString()
is the following function:
import std.array; // ... string indentationString(int level) { return replicate(" ", level * 2); }
The function calls replicate()
from the std.array
module, which makes and returns a new string made up of the specified value repeated the specified number of times.
The destructor can be defined similar to the constructor to produce the closing tag:
~this() { writeln(indentation, "</", name, '>'); }
Here is a test code to demonstrate the effects of the automatic constructor and destructor calls:
import std.conv; import std.random; import std.array; string indentationString(int level) { return replicate(" ", level * 2); } struct XmlElement { string name; string indentation; this(string name, int level) { this.name = name; this.indentation = indentationString(level); writeln(indentation, '<', name, '>'); } ~this() { writeln(indentation, "</", name, '>'); } } void main() { immutable classes = XmlElement("classes", 0); foreach (classId; 0 .. 2) { immutable classTag = "class" ~ to!string(classId); immutable classElement = XmlElement(classTag, 1); foreach (i; 0 .. 3) { immutable gradeElement = XmlElement("grade", 2); immutable randomGrade = uniform(50, 101); writeln(indentationString(3), randomGrade); } } }
Note that the XmlElement
objects are created in three separate scopes in the program above. The opening and closing tags of the XML elements in the output are produced solely by the constructor and the destructor of XmlElement
.
<classes> <class0> <grade> 72 </grade> <grade> 97 </grade> <grade> 90 </grade> </class0> <class1> <grade> 77 </grade> <grade> 87 </grade> <grade> 56 </grade> </class1> </classes>
The <classes>
element is produced by the classes
variable. Because that variable is constructed first in main()
, the output contains the output of its construction first. Since it is also the variable that is destroyed last, upon leaving main()
, the output contains the output of the destructor call for its destruction last.
Copy constructor
Copy construction is creating a new object as a copy of an existing one.
Assuming S
is a struct type, the following are the cases when objects are copied:
- Passing an object to a function that takes by value:
void foo(S s) { // The caller's argument is copied as the parameter // ... }
- Returning an object from a function by value:
S foo() { S result; // ... return result; // The return value is copied to the caller's context }
Note: In practice, that copy is elided if the compiler applies "named return value optimization" (NRVO).
- Copying objects explicitly
There may be a confusion in this case because the assignment operator is used for copying. For example, the second line in the following code is copy construction of the newly created object
a
fromexistingObject
. Theauto
keyword there is the indication that a new object is being defined (and being constructed).auto existingObject = S(); auto a = existingObject; // copy construction a = existingObject; // assignment a = a; // assignment a = S(); // assignment
In contrast, all of the lines following the copy construction line are assignments because
a
already exists as an object on those lines.
By default, copying is automatically handled by the compiler by copying corresponding members of the objects one after the other. Let's assume the following struct definition and the variable a
that is copied from existingObject
:
struct S { int i; double d; } // ... auto existingObject = S(); auto a = existingObject; // copy construction
The automatic copy constructor executes the following steps:
- Copy
a.i
fromexistingObject.i
- Copy
a.d
fromexistingObject.d
An example where the automatic behavior is not suitable is the Student
type defined in the Structs chapter, which had a problem about copying objects of that type:
struct Student { int number; int[] grades; }
Being a slice, the grades
member of that struct
is a reference type. The consequence of copying a Student
object is that the grades
members of both the original and the copy provide access to the same actual array elements of type int
. As a result, the effect of modifying a grade through one of those objects is seen through the other object as well:
auto student1 = Student(1, [ 70, 90, 85 ]); auto student2 = student1; // copy construction student2.number = 2; student1.grades[0] += 5; // this changes the grade of the // second student as well: assert(student2.grades[0] == 75);
To avoid such a confusion, the elements of the grades
member of the second object must be separate and belong only to that object. Such special copy behavior is implemented in the copy constructor.
Being a constructor, the name of the copy constructor is this
as well and it does not have a return type. Its parameter type must be the same type as the struct and must be defined as ref
. Since the source object of a copy should not be modified, it is appropriate to mark the parameter as const
(or inout
). Complementing the this
keyword, it is convenient to name the parameter as that
to signify "this object is being copied from that object":
struct Student { int number; int[] grades; this(ref const(Student) that) { this.number = that.number; this.grades = that.grades.dup; } }
That copy constructor copies the members one by one, especially making sure the elements of grades
are copied with .dup
. As a result, the new object gets its own copy of the array elements.
Note: As described in the "First assignment to a member is construction" section above, those assignment operations are actually copy constructions of the members.
Making modifications through the first object does not affect the second object anymore:
student1.grades[0] += 5; assert(student2.grades[0] == 70);
Although it may make the code less readable, instead of repeating the type of the struct e.g. as Student
as in the code above, the parameter type may generically be written as typeof(this)
for all structs:
this(ref const(typeof(this)) that) { // ... }
Postblit
Postblit is a legacy feature of D, which is discouraged. Newly written code should use copy constructors instead. Postblit is still accepted for backward compatibility but is incompatible with the copy constructor: If the postblit is defined for a type, the copy constructor is disabled.
The legacy way of copying objects in D involves two steps:
- Copying the members of the existing object to the new object bit-by-bit. This step is called blit, short for block transfer.
- Making further adjustments to the new object. This step is called postblit.
The name of the postblit is this
as well and it does not have a return type. To separate it from the other constructors, its parameter list contains the keyword this
:
this(this) { // ... }
The main difference from the copy constructor is that the members of the existing object are already copied (blitted) to the members of the new object by the time the postblit starts executing. Further, there is no that
object to speak of because the postblit is executed on the new object, using only its members. For that reason, all that is needed (and is possible) is to make adjustments to the new object.
The postblit function for the Student
struct could be written as the following:
struct Student { int number; int[] grades; this(this) { // 'number' and 'grades' are already copied at this // point. We just need to make copies of the elements: grades = grades.dup; } }
Assignment operator
Assigment is giving a new value to an existing object:
returnTripDuration = tripDuration; // assignment
Assignment is more complicated from the other special operations because it is actually a combination of two operations:
- Destroying the left-hand side object
- Copying the right-hand side object to the left-hand side object
However, applying those two steps in that order is risky because the original object would be destroyed before knowing that copying will succeed. Otherwise, an exception that is thrown during the copy operation can leave the left-hand side object in an inconsistent state: fully destroyed but not completely copied.
For that reason, the compiler-generated assignment operator acts safely by applying the following steps:
- Copy the right-hand side object to a temporary object
This is the actual copying half of the assignment operation. Since there is no change to the left-hand side object yet, it will remain intact if an exception is thrown during this copy operation.
- Destroy the left-hand side object
This is the other half of the assignment operation.
- Transfer the temporary object to the left-hand side object
No postblit nor a destructor is executed during or after this step. As a result, the left-hand side object becomes the equivalent of the temporary object.
After the steps above, the temporary object disappears and only the right-hand side object and its copy (i.e. the left-hand side object) remain.
Although the compiler-generated assignment operator is suitable in most cases, it can be defined by the programmer. When you do that, consider potential exceptions and write the assignment operator in a way that works even at the presence of thrown exceptions.
The syntax of the assignment operator is the following:
- The name of the function is
opAssign
. - The type of the parameter is the same as the
struct
type. (Like the copy constructor, it may beref const(typeof(this))
as well.) This parameter is often named asrhs
, short for right-hand side. (As in the copy constructor,that
is an appropriate name as well.) - The return type is the same as the
struct
type. - The function is exited by
return this
.
As an example, let's consider a simple Duration
struct where the assignment operator prints a message:
struct Duration { int minute; Duration opAssign(Duration rhs) { writefln("minute is being changed from %s to %s", this.minute, rhs.minute); this.minute = rhs.minute; return this; } } // ... auto duration = Duration(100); duration = Duration(200); // assignment
The output:
minute is being changed from 100 to 200
Assigning from other types
Sometimes it is convenient to assign values of types that are different from the type of the struct
. For example, instead of requiring a Duration
object on the right-hand side, it may be useful to assign from an integer:
duration = 300;
This is possible by defining another assignment operator that takes an int
parameter:
struct Duration { int minute; Duration opAssign(Duration rhs) { writefln("minute is being changed from %s to %s", this.minute, rhs.minute); this.minute = rhs.minute; return this; } Duration opAssign(int minute) { writefln("minute is being replaced by an int"); this.minute = minute; return this; } } // ... duration = Duration(200); duration = 300;
The output:
minute is being changed from 100 to 200 minute is being replaced by an int
Note: Although convenient, assigning different types to each other may cause confusions or bugs.
Disabling member functions
Functions that are declared as @disable
cannot be used.
When there are no sensible default values for the members of a type, its default constructor can be disabled. For example, it may be incorrect for the following type to have an empty file name:
struct Archive {
string fileName;
}
Unfortunately, the compiler-generated default constructor would initialize fileName
as empty:
auto archive = Archive(); // ← fileName member is empty
The default constructor can explicitly be disabled by declaring it as @disable
so that objects must be constructed by one of the other constructors. There is no need to provide a body for a disabled function:
struct Archive { string fileName; @disable this(); // ← cannot be called this(string fileName) { // ← can be called // ... } } // ... auto archive = Archive(); // ← compilation ERROR
This time the compiler does not allow calling this()
:
Error: constructor deneme.Archive.this is not callable because
it is annotated with @disable
Objects of Archive
must be constructed either with one of the other constructors or explicitly with its .init
value:
auto a = Archive("records"); // ← compiles auto b = Archive.init; // ← compiles
The copy costructor, the postblit function, and the assignment operator can be disabled as well:
struct Archive { // ... // Disables the copy constructor @disable this(ref const(typeof(this))); // Disables the postblit @disable this(this); // Disables the assignment operator @disable typeof(this) opAssign(ref const(typeof(this))); } // ... auto a = Archive("records"); auto b = a; // ← compilation ERROR b = a; // ← compilation ERROR
Disabling the copy constructor and the postblit can help in the cases where destructors execute operations that should be performed only once. Copying the objects of such types might cause bugs as the destructor would be executed for multiple copies.
For example, the following destructor intends to write a final "Finishing" message to a file that it uses for logging:
import std.stdio; import std.datetime; struct Logger { File file; this(File file) { this.file = file; log("Started"); } ~this() { log("Finishing"); // ← Intended to be the last message } void log(string message) { file.writefln("%s %s", Clock.currTime(), message); } } void main() { auto logger = Logger(stdout); logger.log("Working inside main"); logger.log("Calling foo"); foo(logger); logger.log("Back to main"); } void foo(Logger logger) { logger.log("Working inside foo"); }
The output of the program shows that the program does not work as intended because the final message appears more than once:
2022-Jan-03 22:21:24.3143894 Started 2022-Jan-03 22:21:24.3144467 Working inside main 2022-Jan-03 22:21:24.3144628 Calling foo 2022-Jan-03 22:21:24.3144767 Working inside foo 2022-Jan-03 22:21:24.3144906 Finishing 2022-Jan-03 22:21:24.3145035 Back to main 2022-Jan-03 22:21:24.3145155 Finishing
The problem is caused because more than one Logger
object is constructed and the destructor is executed for each of them. The object that causes the unintended early "Finishing" message is the parameter of foo
, which is copied because it is by-value.
The simplest solution in such cases is to disable copying and assignment altogether:
struct Logger { @disable this(this); @disable this(ref const(typeof(this))); @disable Logger opAssign(ref const(typeof(this))); // ... }
As Logger
cannot be copied anymore, foo
must be changed to take its parameter by reference:
void foo(ref Logger logger) { // ... }
Summary
- Constructor (
this
) is for preparing objects for use. The compiler-generated default constructor is sufficient in most cases. - The behavior of the default constructor may not be changed in structs;
static opCall
can be used instead. - Single-parameter constructors can be used during type conversions by
to
andcast
. - Destructor (
~this
) is for the operations that must be executed when the lifetimes of objects end. - Copy constructor (
this(ref const(typeof(this)))
) is for defining how an object is copied from an existing one. - Postblit (
this(this)
) is discouraged in new code; it is for adjustments to the object after the members are copied automatically. - Assigment operator (
opAssign
) is for changing values of existing objects. - Member functions can be disabled with
@disable
.