Programming in D – Tutorial and Reference
Ali Çehreli

Other D Resources

Lifetimes and Fundamental Operations

We will soon cover structs, the basic feature that allows the programmer to define application-specific types. Structs are for combining fundamental types and other structs together to define higher-level types that behave according to special needs of programs. After structs, we will learn about classes, which are the basis of the object oriented programming features of D.

Before getting to structs and classes, it will be better to talk about some important concepts first. These concepts will help understand structs and classes and some of their differences.

We have been calling any piece of data that represented a concept in a program a variable. In a few places we have called struct and class variables specifically as objects. I will continue calling both of these concepts variables in this chapter.

Although this chapter includes only fundamental types, slices, and associative arrays; these concepts apply to user-defined types as well.

Lifetime of a variable

The time between when a variable is defined and when it is finalized is the lifetime of that variable. Although it is the case for many types, becoming unavailable and being finalized need not be at the same time.

You would remember from the Name Scope chapter how variables become unavailable. In simple cases, exiting the scope where a variable has been defined would make that variable unavailable.

Let's consider the following example as a reminder:

void speedTest() {
    int speed;               // Single variable ...

    foreach (i; 0 .. 10) {
        speed = 100 + i;     // ... takes 10 different values.
        // ...
    }
} // ← 'speed' is unavailable beyond this point.

The lifetime of the speed variable in that code ends upon exiting the speedTest() function. There is a single variable in the code above, which takes ten different values from 100 to 109.

When it comes to variable lifetimes, the following code is very different compared to the previous one:

void speedTest() {
    foreach (i; 0 .. 10) {
        int speed = 100 + i; // Ten separate variables.
        // ...
    } // ← Lifetime of each variable ends here.
}

There are ten separate variables in that code, each taking a single value. Upon every iteration of the loop, a new variable starts its life, which eventually ends at the end of each iteration.

Lifetime of a parameter

The lifetime of a parameter depends on its qualifiers:

ref: The parameter is just an alias of the actual variable that is specified when calling the function. ref parameters do not affect the lifetimes of actual variables.

in: For value types, the lifetime of the parameter starts upon entering the function and ends upon exiting it. For reference types, the lifetime of the parameter is the same as with ref.

out: Same with ref, the parameter is just an alias of the actual variable that is specified when calling the function. The only difference is that the variable is set to its .init value automatically upon entering the function.

lazy: The life of the parameter starts when the parameter is actually used and ends right then.

The following example uses these four types of parameters and explains their lifetimes in program comments:

void main() {
    int main_in;      /* The value of main_in is copied to the
                       * parameter. */

    int main_ref;     /* main_ref is passed to the function as
                       * itself. */

    int main_out;     /* main_out is passed to the function as
                       * itself. Its value is set to int.init
                       * upon entering the function. */

    foo(main_in, main_ref, main_out, aCalculation());
}

void foo(
    in int p_in,       /* The lifetime of p_in starts upon
                        * entering the function and ends upon
                        * exiting the function. */

    ref int p_ref,     /* p_ref is an alias of main_ref. */

    out int p_out,     /* p_out is an alias of main_out. Its
                        * value is set to int.init upon
                        * entering the function. */

    lazy int p_lazy) { /* The lifetime of p_lazy starts when
                        * it is used and ends when its use
                        * ends. Its value is calculated by
                        * calling aCalculation() every time
                        * p_lazy is used in the function. */
    // ...
}

int aCalculation() {
    int result;
    // ...
    return result;
}
Fundamental operations

Regardless of its type, there are three fundamental operations throughout the lifetime of a variable:

To be considered an object, it must first be initialized. There may be final operations for some types. The value of a variable may change during its lifetime.

Initialization

Every variable must be initialized before being used. Initialization involves two steps:

  1. Reserving space for the variable: This space is where the value of the variable is stored in memory.
  2. Construction: Setting the first value of the variable on that space (or the first values of the members of structs and classes).

Every variable lives in a place in memory that is reserved for it. Some of the code that the compiler generates is about reserving space for each variable.

Let's consider the following variable:

    int speed = 123;

As we have seen in the Value Types and Reference Types chapter, we can imagine this variable living on some part of the memory:

   ──┬─────┬─────┬─────┬──
     │     │ 123 │     │
   ──┴─────┴─────┴─────┴──

The memory location that a variable is placed at is called its address. In a sense, the variable lives at that address. When the value of a variable is changed, the new value is stored at the same place:

    ++speed;

The new value would be at the same place where the old value has been:

   ──┬─────┬─────┬─────┬──
     │     │ 124 │     │
   ──┴─────┴─────┴─────┴──

Construction is necessary to prepare variables for use. Since a variable cannot be used reliably before being constructed, it is performed by the compiler automatically.

Variables can be constructed in three ways:

When a value is not specified, the value of the variable would be the default value of its type, i.e. its .init value.

    int speed;

The value of speed above is int.init, which happens to be zero. Naturally, a variable that is constructed by its default value may have other values during its lifetime (unless it is immutable).

    File file;

With the definition above, the variable file is a File object that is not yet associated with an actual file on the file system. It is not usable until it is modified to be associated with a file.

Variables are sometimes constructed as a copy of another variable:

    int speed = otherSpeed;

speed above is constructed by the value of otherSpeed.

As we will see in later chapters, this operation has a different meaning for class variables:

    auto classVariable = otherClassVariable;

Although classVariable starts its life as a copy of otherClassVariable, there is a fundamental difference with classes: Although speed and otherSpeed are distinct values, classVariable and otherClassVariable both provide access to the same value. This is the fundamental difference between value types and reference types.

Finally, variables can be constructed by the value of an expression of a compatible type:

   int speed = someCalculation();

speed above would be constructed by the return value of someCalculation().

Finalization

Finalizing is the final operations that are executed for a variable and reclaiming its memory:

  1. Destruction: The final operations that must be executed for the variable.
  2. Reclaiming the variable's memory: Reclaiming the piece of memory that the variable has been living on.

For simple fundamental types, there are no final operations to execute. For example, the value of a variable of type int is not set back to zero. For such variables there is only reclaiming their memory, so that it will be used for other variables later.

On the other hand, some types of variables require special operations during finalization. For example, a File object would need to write the characters that are still in its output buffer to disk and notify the file system that it no longer uses the file. These operations are the destruction of a File object.

Final operations of arrays are at a little higher-level: Before finalizing the array, first its elements are destructed. If the elements are of a simple fundamental type like int, then there are no special final operations for them. If the elements are of a struct or a class type that needs finalization, then those operations are executed for each element.

Associative arrays are similar to arrays. Additionally, the keys may also be finalized if they are of a type that needs destruction.

The garbage collector: D is a garbage-collected language. In such languages finalizing an object need not be initiated explicitly by the programmer. When a variable's lifetime ends, its finalization is automatically handled by the garbage collector. We will cover the garbage collector and special memory management in a later chapter.

Variables can be finalized in two ways:

Which of the two ways a variable will be finalized depends primarily on its type. Some types like arrays, associative arrays and classes are normally destructed by the garbage collector some time in the future.

Assignment

The other fundamental operation that a variable experiences during its lifetime is assignment.

For simple fundamental types assignment is merely changing the value of the variable. As we have seen above on the memory representation, an int variable would start having the value 124 instead of 123. However, more generally, assignment consists of two steps, which are not necessarily executed in the following order:

These two steps are not important for simple fundamental types that don't need destruction. For types that need destruction, it is important to remember that assignment is a combination of the two steps above.