Lazy Initialization of const Members
Version | 2 |
Created | 2015-11-14 |
Status | Draft |
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:
- 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.