Best practices of using const

H. S. Teoh hsteoh at quickfur.ath.cx
Fri Jun 21 23:39:20 UTC 2019


On Fri, Jun 21, 2019 at 06:32:33PM +0000, Yatheendra via Digitalmars-d-learn wrote:
[...]
>    struct CostlyComputeResult {
>       ... // data fields
>       // constructor takes compute results, no postblit
>    }
> 
>    struct Wrapper {
>       const (CostlyComputeResult) *cachee = 0;
>       ... // data fields storing compute inputs
>       // constructor takes compute inputs
>       // pointer to function(compute inputs)
>       const ref get() {
>          if (!cachee) {
>             cachee = new(function(inputs));
>          }
>          return cachee;
>       }
>    }
> 
> Hopefully jumping through these hoops is worth the while. Instead,
> maybe just wait until the compiler grows a 'cache pure' function
> qualifier (move constructor required?).

The problem with this is that you cannot use const(Wrapper).

In particular, if you have a function that wants to document that it
does not mutate its argument, you cannot write:

	auto func(in Wrapper data) { ... }

because const(Wrapper) does not allow lazy initialization.  Basically
you have to resort to convention (e.g., name it ReadOnly or something
similar) rather than actually mark it const.  This generally isn't a big
problem if you're using func in isolation, but as soon as you need to
compose func with other const code, you quickly find yourself in a
gordian knot of const incompatibilities that percolate throughout the
entire call chain, because D's const is transitive.

I.e., you want func to interact with other functions that trade in const
data, but you cannot because of the constant(!) need to keep Wrapper
mutable.  The non-constness of Wrapper will then percolate up the call
chain, "tainting" all functions that call it so that they cannot be
marked const, even though *logically* they are const.  This mix is
already bad enough (try it on a non-trivial codebase sometime and see
for yourself), but once you add generic functions to the mix, the whole
thing simply becomes unusable -- because generic functions expect to
write const types as const(T), but that will break if T is Wrapper. OK,
so you can try to make it mutable as a workaround.  But then that breaks
const-ness attribute inference so the generic function becomes
non-const, which in turn recursively causes its callers to be non-const,
etc.  Somewhere at the top of the call chain you'll have a const method
that wants to call a const function, passing some higher-level data
structure that eventually contains Wrapper somewhere deep down -- and it
simply doesn't work without making the *entire* structure mutable, due
to the infectiousness of const.

So as soon as you use Wrapper in any data structure of arbitrary
complexity, the entire thing must be mutable -- otherwise const
percolates all the way down to Wrapper and the caching doesn't work
anymore.

tl;dr: using a wrapper works fine for relatively simple cases.  But as
soon as you add any meaningful complexity to it, the scheme quickly
becomes either impractically convoluted, or outright impossible to use
without a hard cast to cast away const (thereby invoking the lovely UB).


T

-- 
INTEL = Only half of "intelligence".


More information about the Digitalmars-d-learn mailing list