Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I think it was NULL itself. It was a long way until we realised we don't want invalid values and could use the type system to help us use special values safely.


The problem here is that null kinda is consequential of intentional design of the type system itself. In this way, I do think that null was discovered, rather than invented. Remember, C is a kinda "portable assembler" so the constructs in it are based relatively closely to how low level data structures are mapped out in memory.

This is, and continues to be, an incredibly useful feature that makes C and C structs immensely useful concepts. Part of that does need an invalid value[1]. NULL is convenient for this and although there are some very weird JavaScript-trinity-meme-style consequences for this[2], it's such a useful concept that basically all languages that have the ability to construct pointers have a null pointer[3].

The alternative world looks like everyone inventing their own invalid values. Invalid, non-null, pointers are typically MUCH worse than null pointers for debuggability and security. If you unintentionally read/write/execute memory at 0x0 (by far the most common value for NULL), most operating systems will trap this, whereas may not necessarily if 0x12345678 is your invalid value.

[1]: Stuff like IA64 had NaT bits which were effectively an extra bit for what I assume to be this sorta thing. The problem with this is that it costs an extra bit. I don't really know much about IA64, but presumably [NaT 1] + [don't care] would be your null pointers here. I think?

[2]: Really what the standard, in my opinion, should have done is probably not make use of the null pointer UB for many different functions. A lot of compilers took the UB surrounding that to make incredibly dubious "optimizations" that broke stuff with zero actual performance benefit whatsoever

[3]: Yes, even Rust. Although some (again in my opinion) unfortunate design decisions made it so that C-Rust FFI isn't zero cost because of how it treats spans/slices


>[3]: Yes, even Rust. Although some (again in my opinion) unfortunate design decisions made it so that C-Rust FFI isn't zero cost because of how it treats spans/slices

If Rust slices already make you sad, then the thing I'm cooking up will make you cry for days.


Compared to scripting languages with actual tagged types, C doesn't really have a type system, and that's readily apparent to anyone who has written C in the last 43 years and debugged a program written in it.

C pretends types exist with you, but once bytes hit the road, it's all real-life and segmentation faults.


C actually does have a type system and it's one of the bigger issues with the language. If it didn't, unaligned pointers and signed overflow would be totally fine.


Problems with unaligned pointers are basically a hardware defect. Signed overflow is an issue because academics are unhappy computers only can do finite math.

Issue with types and C is while the compiler knows about them the standards committees don't want you to be able to. If C had first class types more people would abandon C++ and that can't be allowed to happen.


The concept of alignment isn't a hardware defect, maybe limitation, but the reason why alignment is a thing has to do with the fact that in chip interconnects transfer blocks. You cannot perform misaligned memory accesses against RAM.

A similar limitation exists when peforming accesses against the cache, but at a much finer granularity.

For bytes, the alignment restriction obviously exists in the 8 bit level. You have one output byte and 64 multiplexer inputs.

If you scale this up to 8 bytes, you will need a lot of 64 Input multiplexers.

But even if you can take the silicon area hit, there is the problem of crossing cache lines and pages.

In the end, you cannot divide memory into blocks and allow primitives to cross those blocks without requesting both blocks at the same time. That's an inefficient waste of resources so why support the wasteful usecase in the first place?


With modern CPU's unaligned accesses only matter when it straddles a cache line address not a register address.


Unaligned pointers are undefined behavior even when the hardware fully supports unaligned access, because you're violating the type's rules.

To be honest, I've never seen much indication that the C and C++ committees are particularly fond of each other. They sometimes coordinate, but they're mostly content letting each other evolve in different directions. C is the way it is only after a long process of evolution away from the bits and bytes of BCPL into the strictly typed language we got from ASNI.


Undefined behavior according to WG14 but perfectly fine on most architectures. Even on architectures that don't support it (what the fuck ARM cortex) the compilers do support it.


Yes, that's exactly the point I've been making. It's undefined today because C has a type system and that type system has this arbitrary rule, not because there are implementation constraints necessitating it (where it could just be implementation defined instead).


By that logic, no natively-compiled language has a type system.

Though I should note that in a way, even some ISAs have one, what with e.g. separate float vs integer registers.


Genuinely curious, how would you handle cases where a value is unset without NULL? This is a legitimate case that happens a lot in eg data modeling


Sum types, of course.


How do you expect to use sum types in assembly? Remember where C came from and why it was designed the way it was.


A naive sum type is just a tag plus a payload. There is no problem here. If you have enums you could have had sum types.

The historical argument and appeal to assembly is illogical here. The only real argument is that niche value optimization is too complex or too clever for the time so even if sum types were in C, nullable pointers would still exist either way.


I remember why C stayed what it is at least: elitism and gatekeeping. And YAGNI, repeated millions of times, of which only the first few were correct.

You're telling me OCaml / Rust / Haskell compile to fairy pixie dust? Obviously their compilers figured it out and it works.


> I remember why C stayed what it is at least: elitism and gatekeeping.

If that was the goal, it failed horribly - the gatekeeping didn't work because the popularity exploded.

> You're telling me OCaml / Rust / Haskell compile to fairy pixie dust? Obviously their compilers figured it out and it works.

I said nothing of the sort.


You asked how sum types work in assembly. I'm telling you that at least 3 compilers figured that part out.


> You asked how sum types work in assembly.

No, I didn't - I asked how sum types were supposed to work in an era of 64KB memory systems.


They don't need extra memory in Rust for the case of nullable pointers.

The boring cases require an enum tag in C too.

By bringing up the one thing that doesn't matter, your argument becomes purely ideological.


You're missing the point - give me a Rust compiler that can run and compile in 64KB memory, then you'll understand that the language C was constrained not just by what the output is running on, but by what the machines of the time could actually handle during compilation.


Borland's PASCAL did it on the IBM PC.

And which modern C compiler fits into 64KB? Even TCC needs 100KB. But that's beside the point. No machine of the last 36 (I'll push my chances, 40) years needs to fit a compiler in 64KB.


> Borland's PASCAL did it on the IBM PC.

That's famously a single-pass compiler. Rust is famously unable to compile in a single pass.

It is not possible to make a borrow-checking language that compiles in a single pass.

> No machine of the last 36 (I'll push my chances, 40) years needs to fit a compiler in 64KB.

Exactly - that's why C is what it is: it wasn't a mistake, they were working under the constraints of the time. My original comment (that you appeared to disagree with) said specifically "Remember where C came from and why it was designed the way it was."

Let me ELI5 it for you: It was specifically designed to emit assembly in a single pass because of the constraints of the time.

WTF does "Hur Dur Rust Goodest!" comments mean in this context?


> That's famously a single-pass compiler. Rust is famously unable to compile in a single pass.

I probably should have replied under the other comment. I was also referring to your

> No, I didn't - I asked how sum types were supposed to work in an era of 64KB memory systems.

But context got lost between replies.

> that's why C is what it is

C famously had a big redesign in 1990. The language of today isn't the same K&R printed.


Pascal had pointers? They could be `nil` too https://www.freepascal.org/docs-html/ref/refse15.html


The thread talked about sum types, which apparently appeared on ALGOL; although I don't know how much memory did an ALGOL compiler need.


How are you going to build sum types in a way where you can interact with assembly or machine code? The CPU doesn't know about that stuff


OCaml / Rust / Haskell.

Apparently they found a way to have the CPU know... about "this stuff".


Sum types map down to reading a tag and doing a comparison against fixed values.

I don't know what to tell you, but you're clearly not cut out to be a software developer in either machine code, assembly or C or any other language if you don't understand something this basic.


Please check your tone down, I'm arguing politely with you but apparently you're so wrapped up in this that you're resorting to ad hominems.

Sum types aren't the be all end all to all issues, for example you can not representer pointer values efficiently with sum types. Even rust does not wrap up pointers with sum types. Now try to go back 37 years to C89 and ask yourself if they were going to require compilers to have stringent checks like the rust compiler does.


Nobody claimed that "sum types are be all end all". I originally responded to "how would you handle cases where a value is unset without NULL" with "sum types" which are trivially presentable with bit masks if memory usage is of big concern (and nowadays in 99.9999% of the cases it genuinely is not).

And, tagged unions are a thing and were a thing for a long time.

Of course it's too late to change all this today; it would have been too late even 20 years ago. But outside of f.ex. Linux kernel and some other super hardcore C libraries, a lot can be done for the world to migrate to safer constructs and away from sentinel values. And that's what languages like Rust do.

Super memory constrained environments have not been the mainstream programming work for decades and now remain limited to embedded / IoT. Not sure what the reservation against sum types is these days.


Yeah and I was trying to explain that sum types don't work for pointers, without a significant performance hit.

No one here is saying C is a great design, but in the context of 60 years ago, it worked out pretty well, and all the language which had additional runtime complexity (Pascal of course, but also Ada and FORTH) struggled because they didn't dial the right level of complexity


That's unnecessarily rude, and untrue in any case. Everyone has to learn stuff sometime, and most people won't naturally run into the implementation details of how higher level languages get translated into machine code.


I agree his tone was not productive but the comment he responded to seemed like a disingenuous argument as well. "The CPU doesn't know about that stuff" is not true -- or it's arguing in bad faith. I mean, hello, tagged unions, all of us with some experience can write a C program that works with those. It's 100% false to say what he said.


The way we do it in modern languages with things like std::optional and even that is not the best example.


And higher level languages that works. But what do you do when you get down to low level C or assembly?

You basically end up with null/0 don’t you?


Rust is a significantly higher level language than C, but it can be used it almost all environments where C is used; provided there's a supported compiler target for it. In (safe) Rust, null is basically a guaranteed compiler optimization. Optional / nullable values are represented via Option<T>, which is a sum type of Some(T) and None. When a reference or other pointer-like value (e.g. Box<T>, an owned heap allocation) is wrapped in Option, the compiler can use the invalid bit patterns of T (such as null) to represent the None variant. This is called niche optimization.

So yes, it's nulls underneath, but the developer never has to think about them.


Eventually you end up with registers that probably allow for 2^N values. But the point is not thinking about the machine executing the instructions, but the construction on top of it that has a safer design.

Seeking performance we've been very prone to avoid abstractions and over and over again have shown why we need the safe abstractions.


They already said:

> use the type system to help us use special values safely

... but this is not the place to explain what a type system is or what sum types/maybe/optional/etc. are.


Meh, I think NULL is fine in C. It's an extra, valid state to represent pointers at no cost. Unlike the more hand holdy languages, it's quite rare for a pointer in C to have the ability to be NULL since, more often than not, it's pointing at something known. It's actually quite rare to see NULL checks unless it's API code or something like that. I can see this being more of a problem in a managed language where anything can be NULL at any time.


NULL as a concept is fine. Inability to declare something as non-null is not.

There is a huge gap between developer expectation "it's pointing at something known" and hard reality confirmed by zillions of CVE. That's the reason optionality is prevalent in modern languages and type checkers (python, typescript), nowdays even Java has sane non-nullable types.


> to represent pointers at no cost

I wouldn't call "cause of bugs and security issues" "no cost".

> it's quite rare for a pointer in C to have the ability to be NULL

As a C programmer for more than 25 years, that is the exact opposite of my experience.


Struct foo has various members, including a bar*. But a foo may or may not be associated with a bar. If there's no associated bar, the bar* pointer is NULL. Seen and done this all the time


The problem with let's get rid of NULL is that it's a real, required state. The vast majority of computing is actually not binary: any real input generally has at least 3 possible states: not set, true and false.

In practice really 4 because "indeterminate" is a reasonable error condition you'd like to know about.

And it keeps increasing anyway: e.g. not set has subcategories: not set due to lack of user input, not set because we're loading state from the backend etc.

NULL is the first expression of that basic problem: it's definitely not enough to eliminate NULL because the first thing which happens is your non pointer default value takes it's place.


What you are describing is option types, which are an entirely valid and very useful construct that helps make programs more rather than less reliable. But you need proper language type system support and compile-time enforcement to make it work, and C does neither of those.


C++ and rust make these optionals ugly. Zig does it right. Zig also forbids null pointers and requires use of optionals.


[flagged]


I don't know man. It's just zero. Dereferencing it just crashes your program (assuming an operating system). This kind of just reads like a fear of pointers. Corrupted/out-of-range pointers are, at least, a real problem.


Is None OK in Python?

NULL in C just doesn’t belong at the end of a string. But IMO having a “there is no value here” designation is not a bad thing.


Python is interpreted so None is always tested for and will throw an exception if used in the wrong context. This is quite different from a SEGVIO.

> But IMO having a “there is no value here” designation is not a bad thing.

Sure ... if it's done via the type system so that errors are caught at compile time. There's a reason that modern languages all either do this or are moving towards doing it. (And a reason that C programmers have no idea what we're talking about when we refer to type systems.)

> NULL in C just doesn’t belong at the end of a string.

Different discussion. (And NUL, not NULL.)


I think you're mixing up the NULL pointer and the NULL (sometimes NUL) character.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: