Best practices of using const

H. S. Teoh hsteoh at quickfur.ath.cx
Fri Jun 21 16:35:50 UTC 2019


On Fri, Jun 21, 2019 at 06:07:59AM +0000, Yatheendra via Digitalmars-d-learn wrote:
> Am I mistaken in saying that we are conflating:
>    "anything that is logically const should be declared const"

No, not in D.  D does not have logical const; it has "physical" const,
which is a strict subset of logical const, and therefore there are some
uses of logical const for which D's const is unsuitable.


>    // makes perfect sense
>    // e.g. the lowest 2, and some branches of the 3rd and 4th, levels
>    // of members (and a subset of the overall methods) in a 5-deep type hierarchy are const
> with:
>    "most code/data should be declared const"
>    // no! isn't efficient code all about mutation?
>    // no grounds for, e.g.: "ideally, no more than 40% of code should be
> doing mutation"

Actually, optimizers work best when there is minimal mutation *in the
original source*.  The emitted code, of course, is free to use mutation
however it wants.  But the trouble with mutation at the source level is
that it makes many code analyses very complex, which hinders the
optimizer from doing what it might have been able to do in the absence
of mutation (or a reduced usage of mutation).  Aliasing is one example
that hampers optimizers from emitting optimal code. Aliasing plus
mutation makes the analysis so complex that the optimizer has a hard
time deciding whether a particular construct can be optimized away or
not.

Having minimal mutation in the original source code allows the optimizer
to make more assumptions, which in turn leads to better optimizations.
It also makes the source code easier to understand.  Paradoxically,
having less mutation in the source code means it's easier for the
compiler to optimize it into mutation-heavy optimal code -- because it
doesn't have to worry about arbitrary mutations in the source code, and
therefore can be free(r) to, e.g., eliminate redundant copies, redundant
movement of data, etc., which ultimately results in in-place
modification of values, i.e., mutation-heavy emitted code.

Conversely, if the source code is heavy on mutations, then the compiler
cannot confidently predict the overall effect of the mutations, and
therefore is forced to err on the safe side of assuming the worst, i.e.,
don't apply aggressive optimizations in case the programmer's mutations
invalidate said optimizations. The result is less optimal code.


[...]
> The inability to have a const caching object seems correct. The way
> around would be to have a wrapper that caches (meh). If that is not
> possible, then maybe caching objects just aren't meant to be const by
> their nature? Isn't memoize a standard library feature? I should look
> at it, but I wouldn't expect it to be const.

It's not as simple as it might seem.  Here's the crux of the problem:
you have an object that logically never changes (assuming no bugs, of
course).  Meaning every time you read it, you get the same value, and so
multiple reads can be elided, etc.. I.e., you want to tell the compiler
that it's OK to assume this object is const (or immutable).

However, it is expensive to initialize, and you'd like it to be
initialized only when it's actually needed, and once initialized you'd
like it to be cached so that you don't have to incur the initialization
cost again.  However, declaring a const object in D requires
initialization, and after initialization it cannot be mutated anymore.
This means you cannot declare it const in the first place if you want
caching.

It gets worse, though.  Wrappers only work up to a certain point.  But
when you're dealing with generic code, it becomes problematic.  Assume,
for instance, that you have a type Costly that's logically const, but
lazily initialized (and cached).  Since you can't actually declare it
const -- otherwise lazy initialization doesn't work -- you have to
declare it mutable.  Or, in this case, declare a wrapper that holds a
const reference to it, say something like this:

	struct Payload {
		// lazily-initialized data
	}

	struct Wrapper {
		const(Payload)* impl;
		...
	}

However, what if you're applying some generic algorithms to it?  Generic
code generally assume that given a type T, if you want to declare a
const instance of it, you simply write const(T).  But what do you pass
to the generic function? If you pass Wrapper, const(Wrapper) means
`impl` cannot be rebound, so lazily initialization fails.  OK, then
let's pass const(Payload) directly.  But that means you no longer have a
wrapper, so you can't have lazy initialization (Payload must be
constructed before you can pass it to the function, thus it must be
eagerly initialized at this point).

It's an impasse.  Cached / lazily-initialized objects and D's const
simply don't mix.  Well, you can try to mix them, but it's like trying
to mix water and oil.  They just don't work well together.


T

-- 
Notwithstanding the eloquent discontent that you have just respectfully expressed at length against my verbal capabilities, I am afraid that I must unfortunately bring it to your attention that I am, in fact, NOT verbose.


More information about the Digitalmars-d-learn mailing list