Fixing C's Biggest Mistake

Timon Gehr timon.gehr at gmx.ch
Sat Dec 31 14:30:36 UTC 2022


On 12/31/22 07:34, Walter Bright wrote:
> On 12/30/2022 1:07 PM, Timon Gehr wrote:
>>> In your description of pattern matching checks in this thread, the 
>>> check was at runtime.
>>> ...
>>
>> No, the check was at compile time.
> 
> The pattern matching is done at run time.
> 
>> The check I care about is the check for _failure_. The check for 
>> _null_ may or may not be _necessary_ depending on the type of the 
>> reference.
> NonNull pointers:
> 
>    int* p = ...;
>    nonnull int* np = isPtrNull(p) ? fatalError("it's null!") : p;
>    *np = 3; // guaranteed not to fail!
> 
> Null pointers:
> 
>    int* p = ...;
>    *p = 3;  // seg fault!
> 
> Which is better? Both cause the program to quit on a null pointer.
> ...

You have deliberately chosen an example where it does not matter because 
your aim was specifically to dereference a possibly null pointer.

I care about this case:

nonnull int* p = ...; // possibly a compile time error
*p = 3; // no runtime check. no seg fault!

Note that the declaration and dereference can be a few function calls 
apart. The further away the two are, the more useful tracking it in the 
type system becomes.


Manual checks can be used to turn possibly null pointers into non-null 
pointers anywhere in the program where there is a sensible way to handle 
the null case separately. This is just a special case of sum types, 
where the compiler checks that you dealt with all cases exhaustively.
The especially efficient tag encoding provided by `null` is just an 
additional small detail.

> 
>> This technology has a proven track record.
> 
> A proven track record of not seg faulting, sure.

Of making people think about, and handle the null case if it is 
necessary at all. I have already told you that my main gripe here is not 
specifically the segfault (though that does not help), it's the fatal 
and implicit nature of the crash.

> A proven trackrecord of 
> no fatal errors at converting a nullable pointer to nonnull, I'm not so 
> sure.
> ...

Converting a nullable pointer to nonnull without handling the null case 
is inherently an unsafe operation. D currently does it implicitly. 
Explicit is better than implicit for fatal runtime errors that will shut 
down your program completely.

Typically you'd mostly use nonnull pointers and not get any fatal 
errors. It is true that if you have nontrivial logic determining whether 
some pointer should be null or not you may have to check that invariant 
at runtime with the techniques present in popular languages, but at 
least it's explicit.

My experience has been that null pointer segfaults usually happen in 
places where either a null pointer is never expected (and a nonnull 
pointer should have been used, making the type system ensure that the 
caller provides one) or there should have been a check, with different 
logic for the null case. I.e., they happen because people failed to 
think about the null case at all. The language encourages this lack of 
thinking by treating all references as non-null references during type 
checking and then crashing at runtime implicitly once the type checker's 
assumptions are inevitably violated.

Nonnull pointers allow expressing such assumptions in the type system. 
They are actually more useful than runtime segfaults and assertion 
failures, because they document expectations and the error will be at 
the place where the bad null pointer originates instead of at the place 
where it was not expected to occur.

Runtime segfaults/assertion failures are actually much more susceptible 
to being papered over by subtly changing a function's interface and 
making it more complex by doing some checking internally and ignoring 
null instead of addressing the underlying issue. This is because it's 
harder to find the root cause, especially in a large undocumented code 
base. Nonnull is compiler-checked documentation and it will direct your 
attention to the function that is actually wrong by default.

> 
>  > Relying on hardware memory protection to catch the null
>  > reference is never necessary,
> 
> If you manually code in a runtime check, sure, you won't need a builtin 
> check at runtime.
> ...

No, you don't need any runtime check at all to dereference a nonnull 
pointer.

nonnull x = new A;

x.y = 3; // runtime checks completely redundant here


>  > because _valid programs should not even compile if
>  > that's the kind of runtime check they would require to ensure type 
> safety_.
> 
> Then we don't need sumtypes with pattern matching?
> ...

That's not what I said. I am specifically talking about _implicit_ 
runtime checks causing a _program panic/segfault_. It's just a bad 
combination for null handling. Bad UX and hardly defensible with 
technical limitations.

>  > The hardware memory protection can still catch compiler bugs I guess.
> 
> Having a hardware check is perfectly valid for checking things.
> ...

Sure, in principle it can still be leveraged for some sort of explicit 
runtime-checked null pointer dereference syntax. Personally, the 
convenience of having the assertion failure tell me where it happened 
(even if I don't happen to be running in a debugger) is probably _by 
far_ worth the additional runtime check in the couple places where it 
would even remain necessary.

Also as Sebastiaan points out, there are actually relevant targets that 
don't give you the check.

> BTW, back in the bad old DOS days, I used to write a lot of:
> 
>      assert(p != NULL);
> 
> It was very effective. But with modern CPUs, this check adds no value, 
> and I removed them.

I have not much to add to this off-topic point. As I told you many times 
by now, I mostly agree here, but I want to be able to move most of this 
checking to compile time instead.

BTW: I really dislike the terminology "nonnull pointer/reference". It's 
a weird inversion of defaults. nonnull is a much better default.


More information about the Digitalmars-d mailing list