Report a bug
If you spot a problem with this page, click here to create a Bugzilla issue.
Improve this page
Quickly fork, edit online, and submit a pull request for this page. Requires a signed-in GitHub account. This works well for small changes. If you'd like to make larger changes you may want to consider using a local clone.

Lazy Initialization of const Members

Version 2
Created 2015-11-14
StatusDraft
Last modified --
Author Marc Schütz

Abstract

This DIP proposes an officially sanctioned way to initialize const members (or mutable members of const structs) lazily by allowing limited mutation of const objects.

Rationale

Lazy initialization is a widely applied technique to 1) defer initialization of a struct or class member until it is actually needed, and 2) cache and reuse the result of an expensive computation. With current const semantics in D, this require objects and structs containing such variables to be mutable. As a consequence, any method that wants to make use of a lazily calculated member cannot be annotated as const, and all functions calling these methods require non-const references to such objects in turn.

Description

A new annotation of member variables is proposed, reusing the existing keyword lazy. A member annotated as lazy triggers the following behaviours:

The first three rules are enforced statically. The last one is checked by an assert() at runtime using a hidden flag member; in -release mode, this check (including the hidden member) is removed.

The second rule is necessary because static immutable objects could be placed in physically read-only memory by the linker and therefore cannot be modified.

The third rule prevents race conditions for implicitly shared immutable objects. Access to shared lazy members must be atomic or otherwise synchronized.

The last rule ensures that external code can never observe a change to the field’s value.

The compiler needs to make sure not to apply optimizations based on the assumption that a lazy member never changes. Use of lazy in this way is therefore @safe. It is, however, limited to values that can be written to the member directly. More complex applications, e.g. memoization depending on parameters using an associative array, or reference counting (which requires more than one mutation) can be implemented by casting the const-ness away. The compiler will still make sure that no breaking optimization are applied, but it can no longer enforce correctness, which makes casting away const un-@safe.

Usage

import std.stdio;
class MyClass {
    void hello() { writeln("Hello, world!"); }
}
struct S {
    // reference types
    lazy private MyClass foo_;
    @safe @property foo() const {
        if(!foo_)
            foo_ = new MyClass;
        return foo_;
    }
    // value types
    lazy private int bar_;
    @safe @property bar() const {
        if(!bar_)
            bar_ = expensiveComputation();
        return bar_;
    }
    // complex
    lazy uint[uint] factorial_;
    @trusted factorial(uint x) const pure {
        if(factorial_ is null)
            factorial_ = createEmptyAA!(uint[uint]); // initialize once
        auto tmp = cast(uint[uint]) factorial_;      // cast to mutable
        if(auto found = x in tmp)
            return *found;
        auto result = computeFactorial(x);
        tmp[x] = result;
        return result;
    }
}

void main() {
    const S s;
    s.foo.hello();
    writeln("bar = ", s.bar);
    writeln("5! = " s.factorial(5));
    writeln("6! = " s.factorial(6));
}

This document has been placed in the Public Domain.