Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Memory Management

D is a language that does not require explicit memory management. However, it is important for a system programmer to know how to manage memory when needed for special cases.

Memory management is a very broad topic. This chapter will introduce only the garbage collector (GC), allocating memory from it, and constructing objects at specific memory locations. I encourage you to research various memory management methods as well as the std.allocator module, which was still at experimental stage at the time of writing this book.

As in some of the previous chapters, when I write variable below, I mean any type of variable including struct and class objects.

Memory

Memory is a more significant resource than other system resources because both the running program and its data are located in the memory. The memory belongs ultimately to the operating system, which makes it available to programs to satisfy their needs. The amount of memory that a program uses may increase or decrease according to the immediate needs of a program. When a program terminates, the memory areas that it has been using are automatically returned back to the operating system.

The memory can be imagined like a large sheet of paper where the values of variables are noted down. Each variable is kept at a specific location where its value is written to and read from as needed. Once the lifetime of a variable ends, its place is used for another variable.

The & (address-of) operator is useful when experimenting with memory. For example, the following program prints the addresses of two variables that are defined next to each other:

import std.stdio;

void main() {
    int i;
    int j;

    writeln("i: ", &i);
    writeln("j: ", &j);
}

Note: The addresses would likely be different every time the program is executed. Additionally, the mere act of taking the address of a variable disables the optimization that would otherwise make the variable live on a CPU register.

As can be seen from the output, the locations of the variables are four bytes apart:

i: 7FFF2B633E28
j: 7FFF2B633E2C

The last digits of the two addresses indicate that i lives in a memory location that is right before the location of j: 8 plus 4 (size of int) makes 12 (C in hexadecimal notation).

The garbage collector

The dynamic variables that are used in D programs live on memory blocks that are owned by the garbage collector (GC). When the lifetime of a variable ends (i.e. it's no longer being used), that variable is subject to being finalized according to an algorithm that is executed by the GC. If nothing else needs the memory location containing the variable, the memory may be reclaimed to be used for other variables. This algorithm is called garbage collection and an execution of the algorithm is called a garbage collection cycle.

The algorithm that the GC executes can roughly be described as the following. All of the memory blocks that can be reached directly or indirectly by pointers (including references) that are in the program roots are scanned. Any memory block that can be reached is tagged as being still in use and all the others are tagged as not being used anymore. The finalizers of objects and structs that live on inaccessible blocks are executed and those memory blocks are reclaimed to be used for future variables. The roots are defined as all of the program stack for every thread, all global and thread-local variables, and any additional data added via GC.addRoot or GC.addRange.

Some GC algorithms can move objects around to keep them together in one place in memory. To preserve program correctness, all of the pointers (and references) that point to such objects are automatically modified to point to the new locations. D's current GC does not do this.

A GC is said to be "precise" if it knows exactly which memory contains pointers and which doesn't. A GC is conservative if it scans all memory as if it were pointers. D's GC is partially conservative, scanning only blocks that contain pointers, but it will scan all data in those blocks. For this reason, in some cases blocks are not ever collected, thereby "leaking" that memory. Large blocks are more likely to be targeted by "false pointers". In some cases it may be recommended to manually free large blocks you are no longer using to avoid this problem.

The order of executing the finalizers is unspecified. For example, a reference member of an object may be finalized before the object that contains that member. For that reason, no class member that refers to a dynamic variable should be accessed inside the destructor. Note that this is very different from the deterministic destruction order of languages like C++.

A garbage collection cycle can be started for various reasons like needing to find space for more data. Depending on the GC implementation, because allocating new objects during a garbage collection cycle can interfere with the collection process itself, all of the running threads may have to be halted during collection cycles. Sometimes this can be observed as a hesitation in the execution of the program.

In most cases the programmer does not need to interfere with the garbage collection process. However, it is possible to delay or dispatch garbage collection cycles as needed by the functions defined in the core.memory module.

Starting and delaying garbage collection cycles

It may be desired to delay the execution of garbage collection cycles during a part of the program where it is important for the program to be responsive. GC.disable disables garbage collection cycles and GC.enable enables them again:

    GC.disable();

// ... a part of the program where responsiveness is important ...

    GC.enable();

However, GC.disable is not guaranteed to prevent a garbage collection cycle from executing: If the GC needs to obtain more memory from the OS, but it cannot, it still goes ahead and runs a garbage collection cycle as a last-ditch effort to gain some available memory.

Instead of relying on garbage collections happening automatically at unspecified times, a garbage collection cycle can be started explicitly using GC.collect():

import core.memory;

// ...

    GC.collect();    // starts a garbage collection cycle

Normally, the GC does not return memory blocks back to the operating system; it holds on to those memory pages for future needs of the program. If desired, the GC can be asked to give unused memory back to the operating system using GC.minimize():

    GC.minimize();
Allocating memory

System languages allow programmers to specify the memory areas where objects should live. Such memory areas are commonly called buffers.

There are several methods of allocating memory. The simplest method would be using a fixed-length array:

    ubyte[100] buffer;    // A memory area of 100 bytes

buffer is ready to be used as a 100-byte memory area. Instead of ubyte, it is also possible to define such buffers as arrays of void, without any association to any type. Since void cannot be assigned any value, it cannot have the .init value either. Such arrays must be initialized by the special syntax =void:

    void[100] buffer = void;    // A memory area of 100 bytes

We will use only GC.calloc from the core.memory module to reserve memory in this chapter. That module has many other features that are useful in various situations. Additionally, the memory allocation functions of the C standard library are avaliable in the std.c.stdlib module.

GC.calloc allocates a memory area of the specified size pre-filled with all 0 values, and returns the beginning address of the allocated area:

import core.memory;
// ...
    void * buffer = GC.calloc(100);
                            // A memory area of 100 zero bytes

Normally, the returned void* value is cast to a pointer of the proper type:

    int * intBuffer = cast(int*)buffer;

However, that intermediate step is usually skipped and the return value is cast directly:

    int * intBuffer = cast(int*)GC.calloc(100);

Instead of arbitrary values like 100, the size of the memory area is usually calculated by multiplying the number of elements needed with the size of each element:

    // Allocate room for 25 ints
    int * intBuffer = cast(int*)GC.calloc(int.sizeof * 25);

There is an important difference for classes: The size of a class variable and the size of a class object are not the same. .sizeof is the size of a class variable and is always the same value: 8 on 64-bit systems and 4 on 32-bit systems. The size of a class object must be obtained by __traits(classInstanceSize):

    // Allocate room for 10 MyClass objects
    MyClass * buffer =
        cast(MyClass*)GC.calloc(
            __traits(classInstanceSize, MyClass) * 10);

When there is not enough memory in the system for the requested size, then a core.exception.OutOfMemoryError exception is thrown:

    void * buffer = GC.calloc(10_000_000_000);

The output on a system that does not have that much free space:

core.exception.OutOfMemoryError

The memory areas that are allocated from the GC can be returned back to it using GC.free:

    GC.free(buffer);

However, calling free() does not necessarily execute the destructors of the variables that live on that memory block. The destructors may be executed explicitly by calling destroy() for each variable. Note that various internal mechanisms are used to call finalizers on class and struct variables during GC collection or freeing. The best way to ensure these are called is to use the new operator when allocating variables. In that case, GC.free will call the destructors.

Sometimes the program may determine that a previously allocated memory area is all used up and does not have room for more data. It is possible to extend a previously allocated memory area by GC.realloc. realloc() takes the previously allocated memory pointer and the newly requested size, and returns a new area:

    void * oldBuffer = GC.calloc(100);
// ...
    void * newBuffer = GC.realloc(oldBuffer, 200);

realloc() tries to be efficient by not actually allocating new memory unless it is really necessary:

GC.realloc is adapted from the C standard library function realloc(). For having such a complicated behavior, realloc() is considered to have a badly designed function interface. A potentially surprising aspect of GC.realloc is that even if the original memory has been allocated with GC.calloc, the extended part is never cleared. For that reason, when it is important that the memory is zero-initialized, a function like reallocCleared() below would be useful. We will see the meaning of blockAttributes later below:

import core.memory;

/* Works like GC.realloc but clears the extra bytes if memory
 * is extended. */
void * reallocCleared(
    void * buffer,
    size_t oldLength,
    size_t newLength,
    GC.BlkAttr blockAttributes = GC.BlkAttr.NONE,
    const TypeInfo typeInfo = null) {
    /* Dispatch the actual work to GC.realloc. */
    buffer = GC.realloc(buffer, newLength,
                        blockAttributes, typeInfo);

    /* Clear the extra bytes if extended. */
    if (newLength > oldLength) {
        import std.c.string;

        auto extendedPart = buffer + oldLength;
        auto extendedLength = newLength - oldLength;

        memset(extendedPart, 0, extendedLength);
    }

    return buffer;
}

The function above uses memset() from the std.c.string module to clear the newly extended bytes. memset() assigns the specified value to the bytes of a memory area specified by a pointer and a length. In the example, it assigns 0 to extendedLength number of bytes at extendedPart.

We will use reallocCleared() in an example below.

The behavior of the similar function GC.extend is not complicated like realloc(); it applies only the first item above: If the memory area cannot be extended in-place, extend() does not do anything and returns 0.

Memory block attributes

The concepts and the steps of a GC algorithm can be configured to some degree for each memory block by enum BlkAttr. BlkAttr is an optional parameter of GC.calloc and other allocation functions. It consists of the following values:

The values of enum BlkAttr are suitable to be used as bit flags that we saw in the Bit Operations chapter. The following is how two attributes can be merged by the | operator:

    const attributes =
        GC.BlkAttr.NO_SCAN | GC.BlkAttr.NO_INTERIOR;

Naturally, the GC would be aware only of memory blocks that are reserved by its own functions and scans only those memory blocks. For example, it would not know about a memory block allocated by std.c.stdlib.calloc.

GC.addRange is for introducing unrelated memory blocks to the GC. The complement function GC.removeRange should be called before freeing a memory block by other means e.g. by std.c.stdlib.free.

In some cases, there may be no reference in the program to a memory block even if that memory block has been reserved by the GC. For example, if the only reference to a memory block lives inside a C library, the GC would normally not know about that reference and assume that the memory block is not in use anymore.

GC.addRoot introduces a memory block to the GC as a root, to be scanned during collection cycles. All of the variables that can be reached directly or indirectly through that memory block would be marked as alive. The complement function GC.removeRoot should be called when a memory block is not in use anymore.

Example of extending a memory area

Let's design a simple struct template that works like an array. To keep the example short, let's provide only the functionality of adding and accessing elements. Similar to arrays, let's increase the capacity as needed. The following program uses reallocCleared(), which has been defined above:

struct Array(T) {
    T * buffer;         // Memory area that holds the elements
    size_t capacity;    // The element capacity of the buffer
    size_t length;      // The number of actual elements

    /* Returns the specified element */
    T element(size_t index) {
        import std.string;
        enforce(index < length,
                format("Invalid index %s", index));

        return *(buffer + index);
    }

    /* Appends the element to the end */
    void append(T element) {
        writefln("Appending element %s", length);

        if (length == capacity) {
            /* There is no room for the new element; must
             * increase capacity. */
            size_t newCapacity = capacity + (capacity / 2) + 1;
            increaseCapacity(newCapacity);
        }

        /* Place the element at the end */
        *(buffer + length) = element;
        ++length;
    }

    void increaseCapacity(size_t newCapacity) {
        writefln("Increasing capacity from %s to %s",
                 capacity, newCapacity);

        size_t oldBufferSize = capacity * T.sizeof;
        size_t newBufferSize = newCapacity * T.sizeof;

        /* Also specify that this memory block should not be
         * scanned for pointers. */
        buffer = cast(T*)reallocCleared(
            buffer, oldBufferSize, newBufferSize,
            GC.BlkAttr.NO_SCAN);

        capacity = newCapacity;
    }
}

The capacity of the array grows by about 50%. For example, after the capacity for 100 elements is consumed, the new capacity would become 151. (The extra 1 is for the case of 0 length, where adding 50% would not grow the array.)

The following program uses that template with the double type:

import std.stdio;
import core.memory;
import std.exception;

// ...

void main() {
    auto array = Array!double();

    const count = 10;

    foreach (i; 0 .. count) {
        double elementValue = i * 1.1;
        array.append(elementValue);
    }

    writeln("The elements:");

    foreach (i; 0 .. count) {
        write(array.element(i), ' ');
    }

    writeln();
}

The output:

Adding element with index 0
Increasing capacity from 0 to 1
Adding element with index 1
Increasing capacity from 1 to 2
Adding element with index 2
Increasing capacity from 2 to 4
Adding element with index 3
Adding element with index 4
Increasing capacity from 4 to 7
Adding element with index 5
Adding element with index 6
Adding element with index 7
Increasing capacity from 7 to 11
Adding element with index 8
Adding element with index 9
The elements:
0 1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 
Alignment

By default, every object is placed at memory locations that are multiples of an amount specific to the type of that object. That amount is called the alignment of that type. For example, the alignment of int is 4 because int variables are placed at memory locations that are multiples of 4 (4, 8, 12, etc.).

Alignment is needed for CPU performance or requirements, because accessing misaligned memory addresses can be slower or cause a bus error. In addition, certain types of variables only work properly at aligned addresses.

The .alignof property

The .alignof property of a type is its default alignment value. For classes, .alignof is the alignment of the class variable, not the class object. The alignment of a class object is obtained by std.traits.classInstanceAlignment.

The following program prints the alignments of various types:

import std.stdio;
import std.meta;
import std.traits;

struct EmptyStruct {
}

struct Struct {
    char c;
    double d;
}

class EmptyClass {
}

class Class {
    char c;
}

void main() {
    alias Types = AliasSeq!(char, short, int, long,
                            double, real,
                            string, int[int], int*,
                            EmptyStruct, Struct,
                            EmptyClass, Class);

    writeln(" Size  Alignment  Type\n",
            "=========================");

    foreach (Type; Types) {
        static if (is (Type == class)) {
            size_t size = __traits(classInstanceSize, Type);
            size_t alignment = classInstanceAlignment!Type;

        } else {
            size_t size = Type.sizeof;
            size_t alignment = Type.alignof;
        }

        writefln("%4s%8s      %s",
                 size, alignment, Type.stringof);
    }
}

The output of the program may be different in different environments. The following is a sample output:

 Size  Alignment  Type
=========================
   1       1      char
   2       2      short
   4       4      int
   8       8      long
   8       8      double
  16      16      real
  16       8      string
   8       8      int[int]
   8       8      int*
   1       1      EmptyStruct
  16       8      Struct
  16       8      EmptyClass
  17       8      Class

We will see later below how variables can be constructed (emplaced) at specific memory locations. For correctness and efficiency, objects must be constructed at addresses that match their alignments.

Let's consider two consecutive objects of Class type above, which are 17 bytes each. Although 0 is not a legal address for a variable on most platforms, to simplify the example let's assume that the first object is at address 0. The 17 bytes of this object would be at adresses from 0 to 16:

     0    1           16
  ┌────┬────┬─ ... ─┬────┬─ ...
  │<────first object────>│
  └────┴────┴─ ... ─┴────┴─ ...

Although the next available address is 17, that location cannot be used for a Class object because 17 is not a multiple of the alignment value 8 of that type. The nearest possible address for the second object is 24 because 24 is the next smallest multiple of 8. When the second object is placed at that address, there would be unused bytes between the two objects. Those bytes are called padding bytes:

     0    1           16   17           23   24   25           30
  ┌────┬────┬─ ... ─┬────┬────┬─ ... ─┬────┬────┬────┬─ ... ─┬────┬─ ...
  │<────first object────>│<────padding────>│<───second object────>│
  └────┴────┴─ ... ─┴────┴────┴─ ... ─┴────┴────┴────┴─ ... ─┴────┴─ ...

The following formula can determine the nearest address value that an object can be placed at:

    (candidateAddress + alignmentValue - 1)
    / alignmentValue
    * alignmentValue

For that formula to work, the fractional part of the result of the division must be truncated. Since truncation is automatic for integral types, all of the variables above are assumed to be integral types.

We will use the following function in the examples later below:

T * nextAlignedAddress(T)(T * candidateAddr) {
    import std.traits;

    static if (is (T == class)) {
        const alignment = classInstanceAlignment!T;

    } else {
        const alignment = T.alignof;
    }

    const result = (cast(size_t)candidateAddr + alignment - 1)
                   / alignment * alignment;
    return cast(T*)result;
}

That function template deduces the type of the object from its template parameter. Since that is not possible when the type is void*, the type must be provided as an explicit template argument for the void* overload. That overload can trivially forward the call to the function template above:

void * nextAlignedAddress(T)(void * candidateAddr) {
    return nextAlignedAddress(cast(T*)candidateAddr);
}

The function template above will be useful below when constructing class objects by emplace().

Let's define one more function template to calculate the total size of an object including the padding bytes that must be placed between two objects of that type:

size_t sizeWithPadding(T)() {
    static if (is (T == class)) {
        const candidateAddr = __traits(classInstanceSize, T);

    } else {
        const candidateAddr = T.sizeof;
    }

    return cast(size_t)nextAlignedAddress(cast(T*)candidateAddr);
}
The .offsetof property

Alignment is observed for members of user-defined types as well. There may be padding bytes between members so that the members are aligned according to their respective types. For that reason, the size of the following struct is not 6 bytes as one might expect, but 12:

struct A {
    byte b;     // 1 byte
    int i;      // 4 bytes
    ubyte u;    // 1 byte
}

static assert(A.sizeof == 12);    // More than 1 + 4 + 1

This is due to padding bytes before the int member so that it is aligned at an address that is a multiple of 4, as well as padding bytes at the end for the alignment of the entire struct object itself.

The .offsetof property gives the number of bytes a member variable is from the beginning of the object that it is a part of. The following function prints the layout of a type by determining the padding bytes by .offsetof:

void printObjectLayout(T)()
        if (is (T == struct) || is (T == union)) {
    import std.stdio;
    import std.string;

    writefln("=== Memory layout of '%s'" ~
             " (.sizeof: %s, .alignof: %s) ===",
             T.stringof, T.sizeof, T.alignof);

    /* Prints a single line of layout information. */
    void printLine(size_t offset, string info) {
        writefln("%4s: %s", offset, info);
    }

    /* Prints padding information if padding is actually
     * observed. */
    void maybePrintPaddingInfo(size_t expectedOffset,
                               size_t actualOffset) {
        if (expectedOffset < actualOffset) {
            /* There is some padding because the actual offset
             * is beyond the expected one. */

            const paddingSize = actualOffset - expectedOffset;

            printLine(expectedOffset,
                      format("... %s-byte PADDING",
                             paddingSize));
        }
    }

    /* This is the expected offset of the next member if there
     * were no padding bytes before that member. */
    size_t noPaddingOffset = 0;

    /* Note: __traits(allMembers) is a 'string' collection of
     * names of the members of a type. */
    foreach (memberName; __traits(allMembers, T)) {
        mixin (format("alias member = %s.%s;",
                      T.stringof, memberName));

        const offset = member.offsetof;
        maybePrintPaddingInfo(noPaddingOffset, offset);

        const typeName = typeof(member).stringof;
        printLine(offset,
                  format("%s %s", typeName, memberName));

        noPaddingOffset = offset + member.sizeof;
    }

    maybePrintPaddingInfo(noPaddingOffset, T.sizeof);
}

The following program prints the layout of the 12-byte struct A that was defined above:

struct A {
    byte b;
    int i;
    ubyte u;
}

void main() {
    printObjectLayout!A();
}

The output of the program showns where the total of 6 padding bytes are located inside the object. The first column of the output is the offset from the beginning of the object:

=== Memory layout of 'A' (.sizeof: 12, .alignof: 4) ===
   0: byte b
   1: ... 3-byte PADDING
   4: int i
   8: ubyte u
   9: ... 3-byte PADDING

One technique of minimizing padding is ordering the members by their sizes from the largest to the smallest. For example, when the int member is moved to the beginning of the previous struct then the size of the object would be less:

struct B {
    int i;    // Moved up inside the struct definition
    byte b;
    ubyte u;
}

void main() {
    printObjectLayout!B();
}

This time, the size of the object is down to 8 due to just 2 bytes of padding at the end:

=== Memory layout of 'B' (.sizeof: 8, .alignof: 4) ===
   0: int i
   4: byte b
   5: ubyte u
   6: ... 2-byte PADDING
The align attribute

The align attribute is for specifying alignments of variables, user-defined types, and members of user-defined types. The value provided in parentheses specifies the alignment value. Every definition can be specified separately. For example, the following definition would align S objects at 2-byte boundaries and its i member at 1-byte boundaries (1-byte alignment always results in no padding at all):

align (2)               // The alignment of 'S' objects
struct S {
    byte b;
    align (1) int i;    // The alignment of member 'i'
    ubyte u;
}

void main() {
    printObjectLayout!S();
}

When the int member is aligned at a 1-byte boundary, there is no padding before it and this time the size of the object ends up being exactly 6:

=== Memory layout of 'S' (.sizeof: 6, .alignof: 4) ===
   0: byte b
   1: int i
   5: ubyte u

Although align can reduce sizes of user-defined types, there can be significant performance penalties when default alignments of types are not observed (and on some CPUs, using misaligned data can actually crash the program).

align can specify the alignment of variables as well:

    align (32) double d;    // The alignment of a variable

However, objects that are allocated by new must always be aligned at multiples of the size of the size_t type because that is what the GC assumes. Doing otherwise is undefined behavior. For example, if size_t is 8 bytes long, than the alignments of variables allocated by new must be a multiple of 8.

Constructing variables at specific memory locations

The new expression achieves three tasks:

  1. Allocates memory large enough for the object. The newly allocated memory area is considered to be raw, not associated with any type or any object.
  2. Copies the .init value of that type on that memory area and executes the constructor of the object on that area. Only after this step the object becomes placed on that memory area.
  3. Configures the memory block so it has all the necessary flags and infrastructure to properly destroy the object when freed.

We have already seen that the first of these tasks can explicitly be achieved by memory allocation functions like GC.calloc. Being a system language, D allows the programmer manage the second step as well.

Variables can be constructed at specific locations with std.conv.emplace.

Constructing a struct object at a specific location

emplace() takes the address of a memory location as its first parameter and constructs an object at that location. If provided, it uses the remaining parameters as the object's constructor arguments:

import std.conv;
// ...
    emplace(address, /* ... constructor arguments ... */);

It is not necessary to specify the type of the object explicitly when constructing a struct object because emplace() deduces the type of the object from the type of the pointer. For example, since the type of the following pointer is Student*, emplace() constructs a Student object at that address:

        Student * objectAddr = nextAlignedAddress(candidateAddr);
// ...
        emplace(objectAddr, name, id);

The following program allocates a memory area large enough for three objects and constructs them one by one at aligned addresses inside that memory area:

import std.stdio;
import std.string;
import core.memory;
import std.conv;

// ...

struct Student {
    string name;
    int id;

    string toString() {
        return format("%s(%s)", name, id);
    }
}

void main() {
    /* Some information about this type. */
    writefln("Student.sizeof: %#x (%s) bytes",
             Student.sizeof, Student.sizeof);
    writefln("Student.alignof: %#x (%s) bytes",
             Student.alignof, Student.alignof);

    string[] names = [ "Amy", "Tim", "Joe" ];
    auto totalSize = sizeWithPadding!Student() * names.length;

    /* Reserve room for all Student objects.
     *
     * Warning! The objects that are accessible through this
     * slice are not constructed yet; they should not be
     * accessed until after they are properly constructed. */
    Student[] students =
        (cast(Student*)GC.calloc(totalSize))[0 .. names.length];

    foreach (int i, name; names) {
        Student * candidateAddr = students.ptr + i;
        Student * objectAddr =
            nextAlignedAddress(candidateAddr);
        writefln("address of object %s: %s", i, objectAddr);

        const id = 100 + i;
        emplace(objectAddr, name, id);
    }

    /* All of the objects are constructed and can be used. */
    writeln(students);
}

The output of the program:

Student.sizeof: 0x18 (24) bytes
Student.alignof: 0x8 (8) bytes
address of object 0: 7F1532861F00
address of object 1: 7F1532861F18
address of object 2: 7F1532861F30
[Amy(100), Tim(101), Joe(102)]
Constructing a class object at a specific location

Class variables need not be of the exact type of class objects. For example, a class variable of type Animal can refer to a Cat object. For that reason, emplace() does not determine the type of the object from the type of the memory pointer. Instead, the actual type of the object must be explicitly specified as a template argument of emplace(). (Note: Additionally, a class pointer is a pointer to a class variable, not to a class object. For that reason, specifying the actual type allows the programmer to specify whether to emplace a class object or a class variable.)

The memory location for a class object must be specified as a void[] slice with the following syntax:

    Type variable =
        emplace!Type(voidSlice,
                         /* ... constructor arguments ... */);

emplace() constructs a class object at the location specified by the slice and returns a class variable for that object.

Let's use emplace() on objects of an Animal hierarchy. The objects of this hierarchy will be placed side-by-side on a piece of memory that is allocated by GC.calloc. To make the example more interesting, we will ensure that the subclasses have different sizes. This will be useful to demonstrate how the address of a subsequent object can be determined depending on the size of the previous one.

interface Animal {
    string sing();
}

class Cat : Animal {
    string sing() {
        return "meow";
    }
}

class Parrot : Animal {
    string[] lyrics;

    this(string[] lyrics) {
        this.lyrics = lyrics;
    }

    string sing() {
        /* std.algorithm.joiner joins elements of a range with
         * the specified separator. */
        return lyrics.joiner(", ").to!string;
    }
}

The buffer that holds the objects will be allocated with GC.calloc:

    auto capacity = 10_000;
    void * buffer = GC.calloc(capacity);

Normally, it must be ensured that there is always available capacity for objects. We will ignore that check here to keep the example simple and assume that the objects in the example will fit in ten thousand bytes.

The buffer will be used for constructing a Cat and a Parrot object:

    Cat cat = emplace!Cat(catPlace);
// ...
    Parrot parrot =
        emplace!Parrot(parrotPlace, [ "squawk", "arrgh" ]);

Note that the constructor argument of Parrot is specified after the address of the object.

The variables that emplace() returns will be stored in an Animal slice later to be used in a foreach loop:

    Animal[] animals;
// ...
    animals ~= cat;
// ...
    animals ~= parrot;

    foreach (animal; animals) {
        writeln(animal.sing());
    }

More explanations are inside the code comments:

import std.stdio;
import std.algorithm;
import std.conv;
import core.memory;

// ...

void main() {
    /* A slice of Animal variables (not Animal objects). */
    Animal[] animals;

    /* Allocating a buffer with an arbitrary capacity and
     * assuming that the two objects in this example will fit
     * in that area. Normally, this condition must be
     * validated. */
    auto capacity = 10_000;
    void * buffer = GC.calloc(capacity);

    /* Let's first place a Cat object. */
    void * catCandidateAddr = buffer;
    void * catAddr = nextAlignedAddress!Cat(catCandidateAddr);
    writeln("Cat address   : ", catAddr);

    /* Since emplace() requires a void[] for a class object,
     * we must first produce a slice from the pointer. */
    size_t catSize = __traits(classInstanceSize, Cat);
    void[] catPlace = catAddr[0..catSize];

    /* Construct a Cat object inside that memory slice and
     * store the returned class variable for later use. */
    Cat cat = emplace!Cat(catPlace);
    animals ~= cat;

    /* Now construct a Parrot object at the next available
     * address that satisfies the alignment requirement. */
    void * parrotCandidateAddr = catAddr + catSize;
    void * parrotAddr =
        nextAlignedAddress!Parrot(parrotCandidateAddr);
    writeln("Parrot address: ", parrotAddr);

    size_t parrotSize = __traits(classInstanceSize, Parrot);
    void[] parrotPlace = parrotAddr[0..parrotSize];

    Parrot parrot =
        emplace!Parrot(parrotPlace, [ "squawk", "arrgh" ]);
    animals ~= parrot;

    /* Use the objects. */
    foreach (animal; animals) {
        writeln(animal.sing());
    }
}

The output:

Cat address   : 7F0E343A2000
Parrot address: 7F0E343A2018
meow
squawk, arrgh

Instead of repeating the steps inside main() for each object, a function template like newObject(T) would be more useful.

Destroying objects explicitly

The reverse operations of the new operator are destroying an object and returning the object's memory back to the GC. Normally, these operations are executed automatically at unspecified times.

However, sometimes it is necessary to execute destructors at specific points in the program. For example, an object may be closing a File member in its destructor and the destructor may have to be executed immediately when the lifetime of the object ends.

destroy() calls the destructor of an object:

    destroy(variable);

After executing the destructor, destroy() sets the variable to its .init state. Note that the .init state of a class variable is null; so, a class variable cannot be used once destroyed. destroy() merely executes the destructor. It is still up to the GC when to reuse the piece of memory that used to be occupied by the destroyed object.

Warning: When used with a struct pointer, destroy() must receive the pointee, not the pointer. Otherwise, the pointer would be set to null but the object would not be destroyed:

import std.stdio;

struct S {
    int i;

    this(int i) {
        this.i = i;
        writefln("Constructing object with value %s", i);
    }

    ~this() {
        writefln("Destroying object with value %s", i);
    }
}

void main() {
    auto p = new S(42);

    writeln("Before destroy()");
    destroy(p);                        // ← WRONG USAGE
    writeln("After destroy()");

    writefln("p: %s", p);

    writeln("Leaving main");
}

When destroy() receives a pointer, it is the pointer that gets destroyed (i.e. the pointer becomes null):

Constructing object with value 42
Before destroy()
After destroy()  ← The object is not destroyed before this line
p: null          ← Instead, the pointer becomes null
Leaving main
Destroying object with value 42

For that reason, when used with a struct pointer, destroy() must receive the pointee:

    destroy(*p);                       // ← Correct usage

This time the destructor is executed at the right spot and the pointer is not set to null:

Constructing object with value 42
Before destroy()
Destroying object with value 42  ← Destroyed at the right spot
After destroy()
p: 7FB64FE3F200                  ← The pointer is not null
Leaving main
Destroying object with value 0   ← Once more for S.init

The last line is due to executing the destructor one more time for the same object, which now has the value S.init.

Constructing objects at run time by name

The factory() member function of Object takes the fully qualified name of a class type as parameter, constructs an object of that type, and returns a class variable for that object:

module test_module;

import std.stdio;

interface Animal {
    string sing();
}

class Cat : Animal {
    string sing() {
        return "meow";
    }
}

class Dog : Animal {
    string sing() {
        return "woof";
    }
}

void main() {
    string[] toConstruct = [ "Cat", "Dog", "Cat" ];

    Animal[] animals;

    foreach (typeName; toConstruct) {
        /* The pseudo variable __MODULE__ is always the name
         * of the current module, which can be used as a
         * string literal at compile time. */
        const fullName = __MODULE__ ~ '.' ~ typeName;
        writefln("Constructing %s", fullName);
        animals ~= cast(Animal)Object.factory(fullName);
    }

    foreach (animal; animals) {
        writeln(animal.sing());
    }
}

Although there is no explicit new expression in that program, three class objects are created and added to the animals slice:

Constructing test_module.Cat
Constructing test_module.Dog
Constructing test_module.Cat
meow
woof
meow

Note that Object.factory() takes the fully qualified name of the type of the object. Also, the return type of factory() is Object; so, it must be cast to the actual type of the object before being used in the program.

Summary