Falsehoods programmers believe about null pointers
106 comments
·February 1, 2025mcdeltat
pradn
> these articles are annoying
You’re being quite negative about a well-researched article full of info most have never seen. It’s not a crime to write up details that don’t generally affect most people.
A more generous take would be that this article is of primarily historical interest.
motorest
> You’re being quite negative about a well-researched article full of info most have never seen.
I don't think this is true. OP is right:
> These articles are annoying because they try to sound smart by going through generally useless technicalities the average programmer shouldn't even be considering in the first place.
Dereferencing a null pointer is undefined behavior. Any observation beyond this at best an empirical observarion from running a specific implementation which may or may not comply with the standard. Any article making any sort of claim about null pointer dereferencing beyond stating it's undefined behavior is clearly poorly researched and not thought all the way through.
pradn
I think you do point to a real issue. The "falsehoods programmers believe about X" genre can be either a) actual things a common programmer is likely to believe b) things a common programmer might not be be knowledgeable enough to believe.
This article is closer to category b. But the category a ones are most useful, because they dispel myths one is likely to encounter in real, practical settings. Good examples of category a articles are those about names, times, and addresses.
The distinction is between false knowledge and unknown unknowns, to put it somewhat crudely.
mcdeltat
You are right, it was overly negative, which was not nice. Read it as a half-rant then. These types of articles are my pet peeve for some reason.
SR2Z
Haha all of the examples in the article are basically "here's some really old method for making address 0 a valid pointer."
This isn't like timezones or zip codes where there are lots of unavoidable footguns - pretty much everyone at every layer of the stack thinks that a zero pointer should never point to valid data and should result in, at the very least, a segfault.
MathMonkeyMan
Useless, but interesting. I used to work with somebody who would ask: What happens with this code?
#include <iostream>
int main() {
const char *p = 0;
std::cout << p;
}
You might answer "it's undefined behavior, so there is no point reasoning about what happens." Is it undefined behavior?The idea behind this question was to probe at the candidate's knowledge of the sorts of things discussed in the article: virtual memory, signals, undefined behavior, machine dependence, compiler optimizations. And edge cases in iostream.
I didn't like this question, but I see the point.
FWIW, on my machine, clang produces a program that segfaults, while gcc produces a program that doesn't. With "-O2", gcc produces a program that doesn't attempt any output.
gerdesj
I think that reasoning about things is a good idea and looking at failure modes is an engineers job. However, I gather that the standard says "undefined", so a correct answer to what "happens with this code" might be: "wankery" (on the part of the questioner). You even demonstrate that undefined status with concrete examples.
In another discipline you might ask what happens what happens when you stress a material near to or beyond its plastic limit? It's quite hard to find that limit precisely, without imposing lots of constraints. For example take a small metal thing eg a paper clip and bend it repeatedly. Eventually it will snap due to quite a few effects - work hardening, plastic limit and all that stuff. Your body heat will affect it, along with ambient temperature. That's before we worry about the material itself which a paper clip will be pretty straightforwards ... ish!
OK, let's take a deeper look at that crystalline metallic structure ... or let's see what happens with concrete or concrete with steel in it, ooh let's stress that stuff and bend it in strange ways.
Anyway, my point is: if you have something as simple as a standard that says: "this will go weird if you do it" then accept that fact and move on - don't try to be clever.
immibis
"undefined" means "defined elsewhere".
mcdeltat
I'm assuming it's meant to be:
std::cout << *p;
?I still think discussing it is largely pointless. It's UB and the compiler can do about anything, as your example shows. Unless you want to discuss compiler internals, there's no point. Maybe the compiler assumes the code can't execute and removes it all - ok that's valid. Maybe it segfaults because some optimisation doesn't get triggered - ok that's valid. It could change between compiler flags and compiler versions. From the POV of the programmer it's effectively arbitrary what the result is.
Where it gets harmful IMO is when programmers think they understand UB because they've seen a few articles, and start getting smart about it. "I checked the code gen and the compiler does X which means I can do Y, Z". No. Please stop. You will pay the price in bugs later.
MathMonkeyMan
> I'm assuming it's meant to be: [...]
Nope, I mean inserting the character pointer ("string") into the stream, not the character to which it maybe points.
Your second paragraph demonstrates, I think, why my former colleague asked the question. And I agree with your third paragraph.
johnnyanmac
>No. Please stop. You will pay the price in bugs later.
indeed. It is called UB because that's basically code for compilers devs to say "welp don't have to worry about changing this" while updating the compiler. What can work in, say, GCC 12 may not work in GCC 14. Or even GCC 12.0.2 if you're unlucky enough. Or you suddenly need to port the code to another platform for clang/MSVC and are probably screwed.
johnnyanmac
>I didn't like this question, but I see the point.
These would be fine interviewing questions if it's meant to start a conversation. Even if I do think it's a bit obtuse from a SWE's perspective ("it's undefined behavior, don't do this") vs. a Computer scientists' perspective you took.
It's just a shame that these days companies seem to want precise answers to such trivia. As if there's an objective answer. Which there is, but not without a deep understanding of your given compiler (and how many companies need that, on the spot, under pressure in a timed interview setting?)
motorest
> These would be fine interviewing questions if it's meant to start a conversation.
I don't agree. They sound like puerile parlour tricks and useless trivia questions, more in line in the interviewer acting defensively and trying too hard to pass themselves as smart or competent instead of actually assessing a candidate's skillset. Ask yourself how frequent those topics pop up in a PR, and how many would be addressed with a 5min review or Slack message.
SAI_Peregrinus
Not quite.
Trivially, `&E` is equivalent to `E`, even if `E` is a null pointer (C23 standard, footnote 114 from section 6.5.3.2 paragraph 4, page 80). So since `&` is a no-op that's not UB.
Also `*(a+b)` where `a` is NULL but `b` is a nonzero integer never dereferences the NULL pointer, but is still undefined behavior since conversions from null pointers to pointers of other types still do not compare equal to pointers to any actual objects or functions (6.3.2.3 paragraph 3) and addition or subtraction of pointers into array objects with integers that produce results that don't point into the same array object are UB (6.5.6).
sgerenser
I prefer: “Falsehoods programmers believe about X” articles with falsehoods considered harmful.
immibis
Are you writing C or C++ code or are you writing, for example C or C++ code for Windows? Because on Windows it's guaranteed to throw an access violation structured exception, for example.
Maxatar
No it's not, even with MSVC on Windows dereferencing a null pointer is not guaranteed to do anything:
Here's a classic article about the very weird and unintuitive consequences of null pointer dereferencing, such as "time travel":
https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=63...
metalcrow
> asking for forgiveness (dereferencing a null pointer and then recovering) instead of permission (checking if the pointer is null before dereferencing it) is an optimization. Comparing all pointers with null would slow down execution when the pointer isn’t null, i.e. in the majority of cases. In contrast, signal handling is zero-cost until the signal is generated, which happens exceedingly rarely in well-written programs.
Is this actually a real optimization? I understand the principal, that you can bypass explicit checks by using exception handlers and then massage the stack/registers back to a running state, but does this actually optimize speed? A null pointer check is literally a single TEST on a register, followed by a conditional jump the branch predictor is 99.9% of the time going to know what to do with. How much processing time is using an exception actually going to save? Or is there a better example?
nickff
The OP is offering terrible advice based on a falsehood they believe about null pointers. In many applications (including the STM32H743 microcontroller that I am currently working on), address zero (which is how "NULL" is defined by default in my IDE) points to RAM or FLASH. In my current application, NULL is ITCM (instruction tightly coupled memory), and it's where I've put my interrupt vector table. If I read it, I don't get an error, but I may get dangerously wrong data.
pfyra
I work with the same mcu. You can set up the MPU to catch null pointer dererences so they don't pass silently.
russdill
Not only that, if you're referencing an element in a very large structure or array, the base address may be zero, but the actual access may be several pages past that.
LorenPechtel
I disagree. You're looking at embedded code which very well might not be running with memory segmentation. If you have no hardware safety you must check your pointers, period. But few of us are in that situation. Personally, I haven't touched an environment without hardware safety in 20 years.
gwbas1c
> Is this actually a real optimization?
No... And yes.
No: Because throwing and catching the null pointer exception is hideously slow compared to doing a null check. In Java / C#, the exception is an allocated object, and the stack is walked to generate a stack trace. This is in addition to any additional lower-level overhead (panic) that I don't understand the details well enough to explain.
Yes: If, in practice, the pointer is never null, (and thus a null pointer is truly an exceptional situation,) carefully-placed exception handlers are an optimization. Although, yes, the code will technically be faster because it's not doing null checks, the most important optimization is developer time and code cleanliness. The developer doesn't waste time adding redundant null checks, and the next developer finds code that is easier to read because it isn't littered with redundant null checks.
mr_00ff00
“Most important optimization is developer time and code cleaniness”
True for 99% of programming jobs, but if you are worried about the speed of null checks, you are in that 1%.
In high frequency trading, if you aren’t first your last and this is the exact type of code optimizations you need for the “happy path”
LorenPechtel
Yup, three is definitely value to the code not doing things it doesn't need to. I also find it clearer if the should-never-happen null check is in the form of an assertion. You know anything being tested in an Assert is a should never happen path, you don't need to consider why it's being done.
gwbas1c
> You know anything being tested in an Assert is a should never happen path, you don't need to consider why it's being done
That's also rather... Redundant in modern null-safe (or similar) languages.
IE, Swift and C# have compiler null checking where the method can indicate if an argument accepts null. There's no point in assertions; thus the null reference exceptions do their job well here.
Rust's option type (which does almost the same thing but trolls love to argue semantics) also is a situation where the compiler makes it hard to pass the "null equivalent." (I'm not sure if a creative programmer can trick the runtime into passing a null pointer where it's unexpected, but I don't understand unsafe Rust well enough to judge that.) But if you need to defend against that, I think Panics can do it.
wging
If you're actually paying a significant cost generating stack traces for NPEs, there's a JVM option to deal with that (-XX:-OmitStackTraceInFastThrow). It still generates a stack trace the first time; if you're able to go search for that first one it shouldn't be a problem for debugging.
LorenPechtel
Conditional jumps tend to carry performance penalties because they tend to trash the look-ahead. And are pretty much forbidden in some cryptographic code where you want constant time execution no matter what.
Also, so long as the hardware ensures that you don't get away with using the null I would think that not doing the test is the proper approach. Hitting a null is a bug, period (although it could be a third party at fault.) And I would say that adding a millisecond to the bugged path (but, library makers, please always make safe routines available! More than once I've seen stuff that provides no test for whether something exists. I really didn't like it the day I found the routine that returned the index of the supplied value or -1 if it didn't exist--but it wasn't exposed, only the return the index or throw if it didn't exist was accessible.) vs saving a nanosecond on the correct path is the right decision.
UncleMeat
In languages like java, jits are really good at optimizing the happy path and outlining these rarely taken cases. In a language like c++ the branch predictor has absolutely no trouble blasting right through branches that are almost always taken in one direction with minimal performance waste.
bjackman
Regardless of whether it's faster, it's an extremely bad idea in a C program, for many of the reasons the author outlines.
It's UB so you are forever gonna be worrying about what weird behaviour the compiler might create. Classic example: compiler infers some case where the pointer is always NULL and then just deletes all the code that would run in that case.
Plus, now you have to do extremely sketchy signal handling.
toast0
Sure, the cost of the check is small, and if you actually hit a null pointer, the cost is much higher if it's flagged by the MMU instead of a simple check.
But you're saving probably two bytes in the instruction stream for the test and conditional jump (more if you don't have variable length instructions), and maybe that adds up over your whole program so you can keep meaningfully more code in cache.
LorenPechtel
More important is the branch predictor. Sometimes you take the hit of a failed predict and you also stuck another entry in the predictor table, making it more likely some other code will suffer a failed branch predict.
oguz-ismail
Signal handling is done anyway. The cost of null pointer checks is a net overhead, if minuscule.
Aloisius
OpenJVM does it, iirc. If the handler is triggered too often at a location, it will swap back to emitting null checks though since it is rather expensive.
Of course, there's a big difference between doing it in a VM and doing it in a random piece of software.
Gibbon1
My takes are
Unlike crummy barely more than a macro assembler compilers of yore. A modern compiler will optimize away a lot of null pointer checks.
Superscalar processors can chew gum and walk at the same time while juggling and hula hooping. That is if they also don't decide the null check isn't meaningful and toss it away. And yeah checking for null is a trivial operation they'll do while busy with other things.
And the performance of random glue code running on a single core isn't where any of the wins are when it comes to speed and hasn't been for 15 years.
heraclius1729
> In both cases, asking for forgiveness (dereferencing a null pointer and then recovering) instead of permission (checking if the pointer is null before dereferencing it) is an optimization. Comparing all pointers with null would slow down execution when the pointer isn’t null, i.e. in the majority of cases. In contrast, signal handling is zero-cost until the signal is generated, which happens exceedingly rarely in well-written programs.
At least from a C/C++ perspective, I can't help but feel like this isn't great advice. There isn't a "null dereference" signal that gets sent--it's just a standard SIGSEGV that cannot be distinguished easily from other memory access violations (memprotect, buffer overflows, etc). In principle I suppose you could write a fairly sophisticated signal handler that accounts for this--but at the end of the day it must replace the pointer with a not null one, as the memory read will be immediately retried when the handler returns. You'll get stuck in an infinite loop (READ, throw SIGSEGV, handler doesn't resolve the issue, READ, throw SIGSEGV, &c.) unless you do something to the value of that pointer.
All this to avoid the cost of an if-statement that almost always has the same result (not null), which is perfect conditions for the CPU branch predictor.
I'm not saying that it is definitely better to just do the check. But without any data to suggest that it is actually more performant, I don't really buy this.
EDIT: Actually, this is made a bit worse by the fact that dereferencing nullptr is undefined behavior. Most implementations set the nullptr to 0 and mark that page as unreadable, but that isn't a sure thing. The author says as much later in this article, which makes the above point even weirder.
saagarjha
You can longjmp out of a signal handler.
fanf2
But that’s likely to be very unsafe, especially in a multithreaded program, or if it relies on stack unwinding for RAII.
heraclius1729
Oh! I didn't know that actually. That's useful information.
alain94040
I would add one more: the address you are dereferencing could be non-zero, it could be an offset from 0 because the code is accessing a field in a structure or method in a class. That offset can be quite large, so if you see an error accessing address 0x420, it's probably because you do have a null pointer and are trying to access a field. As a bonus, the offending offset may give you a hint as to which field and therefore where in your code the bad dereferencing is happening.
nyanpasu64
One interesting failure mode is if (like the Linux kernel) a function returns a union of a pointer or a negative errno value, dereferencing a negative errno gives an offset (below or above zero) different from the field being accessed.
catlifeonmars
Now this is a really interesting one. I’m assuming this trivially applies to index array access as well?
tialaramex
In C, and this article seems to be almost exclusively about C, a[b] is basically sugar for (*((a) + (b)))
C does actually have arrays (don't let people tell you it doesn't) but they decay to pointers at ABI fringes and the index operation is, as we just saw, merely a pointer addition, it's not anything more sophisticated - so the arrays count for very little in practice.
juped
Not just basically sugar, the classic parlor trick is doing 3[arr] or whatever
kevincox
I think this technically wouldn't be a null pointer anymore. As array indexing `p[n]` is defined as `*(p + n)` so first you create a new pointer by doing math on a null pointer (which is UB in C) then dereferencing this new pointer (which doesn't even really exist because you have already committed UB).
caspper69
The article wasn't terrible. I give it a C+ (no pun intended).
Too general, too much trivia without explaining the underlying concepts. Questionable recommendations (without covering potential pitfalls).
I have to say that the discourse here is refreshing. I got a headache reading the 190+ comments on the /r/prog post of this article. They are a lively bunch though.
Hizonner
> Instead of translating what you’d like the hardware to perform to C literally, treat C as a higher-level language, because it is one.
Alternately, stop writing code in C.
uecker
Or catch them using a sanitizer: https://godbolt.org/z/z9WKs5aYv
liontwist
No. I don’t think I will.
mcdeltat
IMO one of the most disappointing things about C: it smells like it should be a straightforward translation to assembly, but actually completely is not because of the "virtual machine" magic the Standard uses which opens the door to almost anything.
Oh you would like a byte? Is that going to be a 7 bit, 8 bit, 12 bit, or 64 bit byte? It's not specified, yay! Have fun trying to write robust code.
tialaramex
Abstract. It's an Abstract machine, not a Virtual machine.
zajio1am
Size of byte is implementation-defined, not unspecified. Why is that a problem for writing robust code? It is okay to assume implementation-defined behavior as long as you are targeting a subset of systems where these assumptions hold, and if you check them at build-time.
bobmcnamara
Ahem, it's specified to not be 7.
keldaris
Luckily, little of it matters if you simply write C for your actual target platforms, whatever they may be. C thankfully discourages the very notion of "general purpose" code, so unless you're writing a compiler, I've never really understood why some C programmers actually care about the standard as such.
In reality, if you're writing C in 2025, you have a finite set of specific target platforms and a finite set of compilers you care about. Those are what matter. Whether my code is robust with respect to some 80s hardware that did weird things with integers, I have no idea and really couldn't care less.
msla
> I've never really understood why some C programmers actually care about the standard as such.
Because I want the next version of the compiler to agree with me about what my code means.
The standard is an agreement: If you write code which conforms to it, the compiler will agree with you about what it means and not, say, optimize your important conditionals away because some "Can't Happen" optimization was triggered and the "dead" code got removed. This gets rather important as compilers get better about optimization.
uecker
No, it has to be at least 8 and this is sufficient to write portable code.
HeliumHydride
C++ has made efforts to fix some of this. Recently, they enforced that signed integers must be two's complement. There is a proposal currently to fix the size of bytes to 8 bits.
mcdeltat
Yes, which is excellent (although 50 years too late, I'll try not to be too cynical...).
The problem is that C++ is a huge language which is complex and surely not easy to implement. If I want a small, easy language for my next microprocessor project, it probably won't be C++20. It seems like C is a good fit, but really it's not because it's a high level language with a myriad of weird semantics. AFAIK we don't have a simple "portable assembler + a few niceties" language. We either use assembly (too low level), or C (slightly too high level and full of junk).
1over137
I'm excited about -fbounds-safety coming soon: https://github.com/llvm/llvm-project/commit/64360899c76c
kerkeslager
That's just not an option in a lot of cases, and it's not a good option in other cases.
Like it or not, C can run on more systems than anything else, and it's by far the easiest language for doing a lot of low-level things. The ease of, for example, accessing pointers, does make it easier to shoot yourself in the foot, but when you need to do that all the time it's pretty hard to justify the tradeoffs of another language.
Before you say "Rust": I've used it extensively, it's a great language, and probably an ideal replacement for C in a lot of cases (such as writing a browser). But it is absolutely unacceptable for the garbage collector work I'm using C for, because I'm doing complex stuff with memory which cannot reasonably be done under the tyranny of the borrow checker. I did spend about six weeks of my life trying to translate my work into Rust and I can see a path to doing it, but you spend so much time bypassing the borrow checker that you're clearly not getting much value from it, and you're getting a massive amount of faffing that makes it very difficult to see what the code is actually doing.
I know HN loves to correct people on things they know nothing about, so if you are about to Google "garbage collector in Rust" to show me that it can be done, just stop. I know it can be done, because I did it; I'm saying it's not worth it.
oguz-ismail
impossible
no serious alternative
IshKebab
Rust and Zig are the serious alternatives for cases where you need a "zero cost" language. If you don't (plenty of C code doesn't) there are endless serious alternatives.
I think you could argue that Zig is still very new so you might not want to use it for that reason, but otherwise there is no reason to use C for new projects in 2025.
oguz-ismail
> Rust
one compiler, two platforms, no spec
> Zig
dollar store rust
tialaramex
For very small platforms, where it's a struggle to have a C compiler because a "long int" of 32 bits is already a huge challenge to implement, let alone "long long int" - stop using high level languages. Figure out the few dozen machine code instructions you want for your program, write them down, review, translate to binary, done.
For the bigger systems where that's not appropriate, you'll value a more expressive language. I recommend Rust particularly, even though Rust isn't available everywhere there's an excellent chance it covers every platform you actually care about.
saagarjha
People wrote and continue to write C for 16-bit platforms just fine
oguz-ismail
[flagged]
xandrius
No true scotsman either.
PoignardAzur
That's not how you're supposed to write a "falsehoods programmers believe about X" article.
The articles that started this genre are about oversimplifications that make your program worse because real people will not fit into your neat little boxes and their user experience with degrade if you assume they do. It's about developers assuming "Oh, everyone has a X" and then someone who doesn't have a X tries to use their program and get stuck for no reason.
Writing a bunch of trivia about how null pointers work in theory which will almost never matter in practice (just assume that dereferencing them is always UB and you'll be fine) isn't in the spirit of the "falsehoods" genre, especially if every bit of trivia needs a full paragraph to explain it.
Blikkentrekker
> In ye olden times, the C standard was considered guidelines rather than a ruleset, undefined behavior was closer to implementation-defined behavior than dark magic, and optimizers were stupid enough to make that distinction irrelevant. On a majority of platforms, dereferencing a null pointer compiled and behaved exactly like dereferencing a value at address 0.
> For all intents and purposes, UB as we understand it today with spooky action at a distance didn’t exist.
The first official C standard was from 1989, the second real change was in 1995, and the infamous “nasal daemons” quote was from 1992. So evidently the first C standard was already interpreted that way, that compilers were really allowed to do anything in the face of undefined behavior. As far as I know
bitwize
Nowadays, UB is used pretty much as a license to make optimizer go brrrr. But back in the day, I think it was used to allow implementations wiggle room on whether a particular construct was erroneous or not -- in contrast to other specifications like "it is an error" (always erroneous) or "implementation-defined behavior" (always legitimate; compiler must emit something sensible, exactly what is not specified). In the null pointer case, it makes sense for kernel-mode code to potentially indirect to address 0 (or 0xffffffff, or whatever your architecture designates as null), while user-space code can be reasonably considered never to legitimately access that address because the virtual memory never maps it as a valid address. So accessing null is an error in one case and perfectly cromulent in the other. So the standard shrugs its shoulders and says "it's undefined".
lmm
The original motivation was to permit implementations to do the reasonable, friendly thing, and trap whenever the program dereferences a null pointer. Since C compilers want to reorder or elide memory accesses, you can't really define explicit semantics for that (e.g. you want it to be ok to move the memory access before or after a sequence point) - the JVM has to do a lot of work to ensure that it throws NullPointerException at the correct point when it happens, and this slows down all programs even though no-one sane has their program intentionally trigger one. But the intention was to permit Java-like behaviour where your code would crash with a specific error immediately-ish, maybe not on the exact line where you dereferenced null but close to it. Ironically compiler writers then took that standard and used it to do the exact opposite, making null dereference far more dangerous than even just unconditionally reading memory address 0.
uecker
Compiler writers did both: https://godbolt.org/z/z9WKs5aYv
megous
Dereferencing a null pointer is how I boot half of my systems. :D On Rockchip platforms address 0 is start of DRAM, and a location where [U-Boot] SPL is loaded after DRAM is initialized. :)
SAI_Peregrinus
That's not a null pointer. Address `0` can be valid. A null pointer critically does not compare equal to any non-null pointer, including a pointer to address 0 on platforms where that's allowed.
> An integer constant expression with the value `0` , such an expression cast to type `void *` , or the predefined constant `nullptr` is called a null pointer constant ^69) . If a null pointer constant or a value of the type `nullptr_t` (which is necessarily the value `nullptr` ) is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
C 23 standard 6.3.2.3.3
Also this is point 6 in the article.
megous
It pretty clearly is a null pointer on my machine. What's null pointer on some HP mainframe from 70's is kinda irrelevant to how I boot my systems.
userbinator
"Falsehoods programmers believe" is the "considered harmful" of the modern dogma cult.
butter999
It's a genre. It's neither dogmatic, modern, nor unique to programming.
"falsehoods 'falsehoods programmers believe about X' authors believe about X"...
All you need to know about null pointers in C or C++ is that dereferencing them gives undefined behaviour. That's it. The buck stops there. Anything else is you trying to be smart about it. These articles are annoying because they try to sound smart by going through generally useless technicalities the average programmer shouldn't even be considering in the first place.