Best practices of using const

Yatheendra 3df4 at gmail.ru
Fri Jun 21 18:32:33 UTC 2019


That is a comprehensive reply. No pointers to other material 
required :-)

On Friday, 21 June 2019 at 16:35:50 UTC, H. S. Teoh wrote:
> On Fri, Jun 21, 2019 at 06:07:59AM +0000, Yatheendra via 
> Digitalmars-d-learn wrote:
> 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).
> [...]

(aside: I hope we don't end up advocating the Haskell/Erlang way 
or the Clojure way!)

Yes, the hindrances of non-const code are documented (are most 
programmers listening!). I was only pointing out that mutation 
being part of the design limits what can be logically const. Is 
the trade-off clear, between (mythical) guaranteed C++-like-const 
at all the points we remember to put it, versus guaranteed 
D-const at the fewer points we manage to put it? Does D-const 
distort the design (but you get all the optimizations possible in 
that scenario)?

>> 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).
>

I should check on std memoize & maybe code something up for 
understanding before writing more than this - would you mind 
pointing to an example range algorithm that we would have trouble 
passing a caching wrapper to?

I hadn't considered pointers as an option. Why wouldn't the 
following work, if expressible in D?
    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?).


More information about the Digitalmars-d-learn mailing list