DIP85

From D Wiki
Jump to: navigation, search
Title: Lazy Initialization of const Members
DIP: 85
Version: 2
Status: Draft
Created: 2015-11-14
Last Modified: 2015-11-14
Author: Marc Schütz
Links: Discussion in the forum

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:

  • it is required to be private
  • no static immutable objects with a lazy member may be created
  • dynamically created immutable objects with lazy members are allowed if all lazy members are marked as shared
  • it can be assigned to even if it is const, but only if it has been read at most once

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));
}

Copyright

This document has been placed in the Public Domain.