Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Comments on Why not call nullptr NULL?

Parent

Why not call nullptr NULL?

+13
−0

In C++11 the nullptr keyword was added as a more type safe null pointer constant, since the previous common definition of NULL as 0 has some problems.

Why did the standards committee choose not to call the new null pointer constant NULL, or declare that NULL should be #defined to nullptr?

History

0 comment threads

Post
+11
−0

It's a long story.

Once upon a time, there were three ways to express a null pointer constant:

  • NULL
  • 0
  • ((char *) 0)

This was true at least as far back as Unix V7 (1979). Maybe even further back in time; I don't have older Unix sources at hand.

void * still didn't exist. NULL itself resolved to one of the other two.

Over time, void * was invented, and started to be used. However, it was still recent enough that it wasn't considered as written in stone. The ((char *) 0) null pointer constant started to be replaced by ((void *) 0).

Then C++ was invented, and questioned the goodness and safety of the new void *, as it could be used to circumvent type safety. C++ decided that instead of allowing 2-way conversions, it would only allow one way.

0 had been an unsafe null-pointer constant, especially in variadic function calls. And even in other places it was quite unreadable, as it could be easily confused with an integer 0. Eventually, C moved into using NULL everywhere, and defining NULL as ((void *) 0), with only residual cases of the older 0. And that use was eventually standardized into the POSIX definition of NULL, which requires that it is defined as "the value 0 cast to type void *".

However, C++ had already forked their idea of void * with distinct implicit conversions, which meant they couldn't use this better definition of NULL (it wouldn't implicitly convert to any other pointer, which is a key feature of a null pointer constant). C++ had to stick to the worst possible null pointer constants: either a literal 0, or a NULL that would itself expand to 0. In fact, most C++ code avoided using NULL (due to some irrational aversion to macros), and decided to stick with the absolute worst possible null pointer constant: 0. See https://www.stroustrup.com/bs_faq2.html#null. Ironically, in their attempt to have a safer language, they had an unsafer null pointer constant, for decades.

In the C world, almost everybody had moved to the right definition of NULL. Only a few groups had stayed with the old #define NULL 0, and only because they hadn't really reasoned about it too much. For that reason, ISO C still allowed the low-quality definition of NULL; even though mostly every vendor had the good one.

After several decades of having a broken set of null pointer constants, C++ decided to fix their self-inflicted bug. C++11 invented nullptr (and nullptr_t). C didn't care at all, because it was not a problem in C.

At some point, some people raised the false premise that C code using NULL was unsafe, because it was allowed to be defined to 0. The right thing to do would have been to follow POSIX, and require in ISO C that NULL be defined as ((void *) 0). That would have required a little bit of work, talking to those implementations that had the bad definition, and convince them to move on.

Those people didn't feel like doing that job, and had also some conflict of interest, since they were proponents of a common core between C++ and C, being (also) C++ programmers. Members of the C standard committee (WG14) proposed something that was less work: pick C++'s nullptr in C.

In the end, the feature was voted into the standard, mainly by C++ proponents, and some pressure by a few proprietary compiler vendors that didn't want to fix their broken NULL. https://thephd.dev/c23-is-coming-here-is-what-is-on-the-menu#n3042---introduce-the-nullptr-constant.

One co-author of the paper that introduced nullptr in C23, n3042, wrote the following:

Someone recently challenged me, however: they said this change is not necessary and bollocks, and we should simply force everyone to define NULL to be void*. I said that if they’d like that, then they should go to those vendors themselves and ask them to change and see how it goes. They said they would, and they’d like a list of vendors defining NULL to be 0. Problem: quite a few of them are proprietary, so here’s my Open Challenge:

if you (yes, you!!) have got a C standard library (or shim/replacement) where you define NULL to be 0 and not the void-pointer version, send me a mail and I’ll get this person in touch with you so you can duke it out with each other. If they manage to convince enough vendors/maintainers, I’ll convince the National Body I’m with to write a National Body Comment asking for nullptr to be rescinded. Of course, they’ll need to not only reach out to these people, but convince them to change their NULL from 0 to ((void*)0), which. Well.

Good luck to the person who signed up for this.

I did my work, and researched which open-source implementations were really defining NULL as 0. I could only find exactly one. I talked to them, and gave arguments of why it would be reasonable to fix their definition. https://github.com/cc65/cc65/issues/1823

Very quickly, the vendor agreed with the arguments, and changed their implementation. The co-author of the paper had to fulfill its promise, and raise a National Body Comment against its own paper, soliciting the removal of nullptr from C23. However, C++ proponents in WG14 voted in favor of ignoring the comment (not even giving reasons, other than not wanting to question again the topic), and the feature stayed in C23. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3077.htm (search for NEN/NL5).

From the reasons that the papers that added the feature in C23 cited, let's break them down, and explain why they were invalid:

  • Compatibility with C++

    Code using NULL had always been valid C++. And C++ could #define NULL nullptr, if they really cared about C/C++ compatibility. They never cared about that, and thus never did. C++ considers C to be a misdesigned language that can only be fixed by making it be C++.

  • _Generic.

    The paper says that having nullptr be of type nullptr_t would be better for _Generic, because it would allow behaving differently for a null pointer constant input. But why should a function behave differently for a null pointer constant than for a run-time null pointer? That makes no sense at all. Also, _Generic selections would still have to take care of users passing NULL, after all, so it's just duplicating the number of branches that have to be written, making it worse.

  • Vendors refusing to fix their implementation.

    Did anybody really try? I mean, with good arguments? I have doubts. It only took me a few days to fix the only FOSS vendor that we could find. If anyone gives me the address of the maintainers of the proprietary (undisclosed) vendors that refused to do so, I'll try to convince them. Nobody gave me their addresses. I think ISO should have just told them to upgrade or die; or assume that they would not conform to C23.

History

2 comment threads

(void)0 wasn't very smart either. (13 comments)
Ouch. While I'd like to take what you wrote with a grain of salt, your story sounds too similar to wh... (2 comments)
(void)0 wasn't very smart either.
Lundin‭ wrote 3 months ago

There's a bit of bias here I think: while C++ NULL was always very problematic, so was NULL in C even after void pointers. (void*)0 was always bad because from C89 to C17, we have that (C17 6.3.2.3) "If a null pointer constant 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. Except we also have the fact that void* are only implicitly convertible to/from object pointers, which was always the case. Never function pointers. So we can't strictly initialize a function pointer to NULL in C, even though the standard always said NULL should be useful for function pointers too. In case anyone wonders which NULL version that's good to use & sensibly implemented, here is a summary: Pre C standard NULL: bad. Post C standard/POSIX NULL: bad. Pre C++11 NULL: bad. Post C++11 NULL: bad. C23 NULL: bad. :)

alx‭ wrote 3 months ago

Huh! This is a good point.

I think this could be considered a defect in the standard, since, as you say (and quote), some paragraphs expect it to be usable with function pointers.

Since null pointer constants are already special (and not really bound by the rules of void*), we could add an implicit conversion to function pointers too. Consider the rules for the conditional operator, which behaves differently for a null pointer constant than for void*. The same could be done with function pointers.

alx‭ wrote 3 months ago

Actually, after checking C23 (n3220), null pointer constants are implicitly convertible to any pointers. See 6.5.17.2p1 bullet 6:

the left operand is an atomic, qualified, or unqualified pointer, and the right operand is a null pointer constant or its type is nullptr_t; or

This text was essentially there already in C99 (6.5.16.1p1 bullet 5):

the left operand is a pointer and the right is a null pointer constant; or

And also there in C89 (3.3.16.1/Constraints/bullet 5):

the left operand is a pointer and the right is a null pointer constant.

Thus, implicit conversions from NULL to function pointers are (and have always been) allowed.

Lundin‭ wrote 3 months ago

alx‭ It is still strange. Compilers have to implement an exception for (void*)0 so we have the situation where void (*fptr)(void) = (void*)0; is fine and void (*fptr)(void) = (void*)1; is a compiler error. This wasn't given enough thought in the early C standards and probably too much thought in C23. At least in C23 we can write void (*fptr)(void) = false; :)

alx‭ wrote 3 months ago · edited 3 months ago

Lundin‭ I agree it's a bit surprising that NULL is special in that it doesn't follow the rules of its own type, void*. But that's something that we can't get rid of (for historic reasons), and isn't really problematic (other than to compiler implementors).

(And nullptr isn't better, so we don't yet know any better approach.)

Lundin‭ wrote 3 months ago · edited 3 months ago

Except if I write if(str== 0) by accident when I actually meant to do if(*str == 0) (which would have been better written as if(*str=='\0')). So yeah null pointer constants remain dysfunctional and dangerous in all versions of C (and C++).

alx‭ wrote 3 months ago · edited 3 months ago

0 as a null pointer constant is really bad. That's why I pushed for the addition of -Wzero-as-null-pointer-constant in GCC in C mode (before, it was only in C++ mode). Since GCC 15, -Werror=zero-as-null-pointer-constant will prevent that. It was implemented in 236c0829ee21 (2024-11-13; "c: add Wzero-as-null-pointer-constant [PR117059]").

BTW, I recommend checking for the end of a string with streq(str, ""). That avoids this and other problems.

#define streq(...)  (0 == strcmp(__VA_ARGS__))

Any decent optimizer will produce the same binary, and it will improve the readability and robustness. I try to never do any manual byte operations on strings, and instead use string functions as much as I can.

Lundin‭ wrote 3 months ago

Except home-brewed macros tend to be far more dangerous than raw C code :) For example strcmp can't be used on non-sanitized data whereas a raw character check can do that, so your macro is actually far less robust. But that's another story.

alx‭ wrote 3 months ago

Lundin‭

Now I'm curious. What are the dangers of strcmp(3) that you're considering?

streq(p, "") is limiting strcmp(3) to a read of 1 byte (because every implementation of strcmp(3) will not read more than the length of the shortest string argument +1).

But even if it were comparing two strings for equality, I don't expect strcmp(3) to have any dangers. As long as the string is properly terminated, it's okay. If you're within kernel code, or any other privileged code, and you're getting a pointer from unprivileged code, you might doubt the termination of the string, but other than that, I don't think it's very problematic.

Lundin‭ wrote 3 months ago

alx‭ A manual loop iterating byte by byte can have a maximum number of iterations and then stop if no null terminator was spotted, or otherwise stop if something is regarded as wrong. Whereas most str... functions assume that you pass them a nul-terminated string, so they were never meant to be used on non-sanitized data. Essentially it is this whole discussion here: Is strcpy dangerous and what should be used instead?

alx‭ wrote 3 months ago · edited 3 months ago

But even unsanitized data is held on strings. Most of the text that a program gets comes from a file or stdin with fgets(3) or similar. It can also come from the command line with argv, which also guarantees strings. Or it can come from the environment (getenv(3)), which should also guarantee null-terminated strings.

Those strings may contain too-long strings, or strings that contain dangerous characters, but they are still null-terminated strings. It's actually rare to find non-strings in a program. Except in a kernel or other similar privileged code, where attackers can pass you pointers to malformed data.

Lundin‭ wrote 3 months ago

This is getting quite off-topic :) Maybe this discussion is more suited for the Discord chat or something.

alx‭ wrote 3 months ago

I don't use chats, but feel free to send me an email (you already have my address, I believe). :)