Can non-nullable references be implemented as a library?

Adam Burton adz21c at gmail.com
Sun Nov 7 18:42:08 PST 2010


Kagamin wrote:

> Simen kjaeraas Wrote:
> 
>> Worth adding:
>> Even if non-null references/pointers cannot be perfectly implemented in a
>> library, they may still be a worthwhile addition to Phobos, to mark
>> function arguments and the like.
> 
> Hmm... this doesn't work:
> 
> struct NonNull(T)
> {
> T value;
> this(T v)
> {
> assert(value);
> value=v;
> }
> }
> 
> class A {}
> 
> void foo(NonNull!A a){}
> 
> void goo()
> {
> A a=new A();
> foo(a);
> }
> 
> test.d(23): Error: function test.foo (NonNull!(A) a) is not callable using
> argument types (A) test.d(23): Error: cannot implicitly convert expression
> (a) of type test.A to NonNull!(A)
The above seems correct to me. You are assigning a nullable to a non-
nullable so you force the user to assess that is correct and provide an 
override. Based on that I've had a crack at this myself.

http://myweb.tiscali.co.uk/adz21c/nonnull.d
http://myweb.tiscali.co.uk/adz21c/test.d

Upto now I've only concentrated on mutable class references, but I figured I 
would post what I have up to now and see what people think.

I've pulled NonNull (NN to save my fingers) construction out into a separate 
function. toNN performs the runtime null check rather than the struct 
constructor. This allows us to use opAssign to convert NN!Derived to 
NN!Super without a null check, you only check for nulls when crossing from 
the nullable world to non-nullable using toNN. It also means code like below 
is not possible without explicitly saying I want to cross from nullable to 
non-nullable (using toNN) which offers some sort of compile time check (not 
as good as a compiler checking for "if is null" but it is something).

A a = null;
NN!A b = a;	// will not compile
NN!A b = toNN(a);	// inserts the null runtime check
NN!A c = b; // No null check as there is no need.

I've attempted where possible to try and keep it looking like a reference 
(as you can see above). So far I have come across a few snags in my plan.

1. Default struct constructor.
This means the NN can be created without assigning a value. I have tried to 
get around this issue somewhat by adding a null check in the invariant but 
it seems the invariant is not called when using the default constructor. 
Should it be or should it not? If it is then atleast while contracts are 
enabled we get to know about uninitialised NNs.

2. opAssign and alias this on function parameters
a. For assignment I used opAssign to perform a conversion from NN!B to NN!A. 
This doesn't work for function parameters of NN!A when passing NN!B so I am 
forced to use toNN adding unnecessary runtime check and nasty looking code.
b. When passing NN!A to A I assume the alias this kicks in and passes 
NN!A.value to A. However on function parameters this is not happening so I 
am forced to manually call NN!A.value.

3. Assigning new gets checked
Below is annoying, clearly it does not need a null check.

NN!A a = toNN(new A());

I figure a function that does not do a null check *could* be offered for 
this situation, but unfortunately that also destroys whatever guarantuees 
you get with NN. The other idea I have had is somehow passing in the "new 
A()" as an expression and evaluating if the expressions purpose is to create 
an object then remove the null check, but I don't know how to do that.

4. Double checking nulls

foo(A a)
{
   if (a is null)  // check one
      doSomething();
   else
      bar(toNN(a));  // check two
}

bar(NN!A a) {}

If the user checks for null before assigning nullables to NN then 2 null 
checks are performed, one by the user and another by toNN. Would the 
compiler be smart enough to remove the redundancy? If not then the only way 
round it I can see is allow a delegate to be passed to toNN which holds the 
isnull code path. Problem with this is I think it would make code quite 
difficult to read. Otherwise we suck it up.

5. Consider if someone is altering code with toNN in it, the toNN will hide 
a bug (if they removed a required check) till runtime that could be 
identified at compile time. But I guess thats the diff between a library 
version and compiler version. At least the code will die earlier at runtime 
though.

Thoughts up to now? Am I barking up completely the wrong tree?


More information about the Digitalmars-d mailing list