DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on

nullptr in C23

#c

Introduction

Since its creation in the 1970s, C has used the preprocessor to define a macro for the “null pointer.” Originally, it was defined as:

#define NULL 0 
Enter fullscreen mode Exit fullscreen mode

or perhaps 0L since void and the ability to use void* weren’t added until C89. This required C to have a special case for the integer literals 0 and 0L (and 0LL once long long was added in C99, as well as their unsigned variants) allowing only them to be converted to any pointer type implicitly:

char *p = 0; // OK char *q = 1; // error 
Enter fullscreen mode Exit fullscreen mode

For simplicity, going forward, whenever I mention 0 as a possible definition of NULL, interpret that to mean either 0 or any one of 0U, 0L, 0UL, 0LL, or 0ULL.

Since C89, NULL is typically instead defined as:

#define NULL ((void*)0) 
Enter fullscreen mode Exit fullscreen mode

which is better; however, some platforms still define it simply as 0. This lack of consistency can sometimes cause portability problems, i.e., code that relies upon NULL defined one way breaks on platforms where it’s defined a different way.

Problems

One case where how NULL is defined can matter is with functions having variadic arguments:

printf( "%p\n", NULL ); // may be undefined behavior 
Enter fullscreen mode Exit fullscreen mode

If NULL is defined as 0 and sizeof(int) < sizeof(void*), then half the value of the argument might be garbage, but definitely undefined behavior.

While you’d most likely never call printf with NULL like that, you could have your own variadic function that expects a NULL-terminated list of pointers and have the same problem.

Another case is if you use _Generic:

#define F(X) \ _Generic( (X), \ int : f_int, \ void*: f_void_ptr \ )( (X) ) 
Enter fullscreen mode Exit fullscreen mode

For F(NULL), which function gets called depends on how NULL is defined.

nullptr

To fix these problems, C23 borrowed the nullptr keyword from C++ that’s a literal for the null pointer. Like void* and 0, nullptr implicitly converts to any type of pointer; unlike NULL, it’s guaranteed to be a pointer.

Why not just fix NULL, i.e., always define it to be ((void*)0)? The proposal mentions it, but doesn’t elaborate. My guess is that if your code continues to use NULL, you may or may not get the “fixed” version depending on the platform, so you’re no better off. You could test for NULL at configure-time, but it’s not clear what you’d do if it’s not the NULL you want; plus testing for things at configure-time is more of a pain. If you use nullptr, you’re obviously getting what you want. It also increases interoperability with C++.

The use of nullptr fixes cases when used with variadic arguments, but what about _Generic? To fix that, nullptr also needs to have a distinct type. Curiously, the stddef.h header now includes:

#define nullptr_t typeof(nullptr) 
Enter fullscreen mode Exit fullscreen mode

that is, it defines nullptr_t using typeof to be whatever distinct type the compiler uses internally for nullptr. It may seem a bit weird not to define nullptr_t as a keyword also, but it works. Given that, you can now use it in _Generic:

#define F(X) \ _Generic( (X), \ int : f_int \ void* : f_void_ptr, \ nullptr_t: f_null_ptr \ )( (X) ) 
Enter fullscreen mode Exit fullscreen mode

The only time f_null_ptr would get called is if you passed nullptr.

Well, that’s not entirely true. You could also declare a variable of type nullptr_t and pass that. But the only value such a variable can ever legally have is nullptr, so I can’t think of a legitimate reason for ever declaring and using such a variable.

Conclusion

The addition of nullptr eliminates an old wart in C fixing a few portability problems and increases interoperability with C++. If your compiler supports it, you should use nullptr in new C code.

Top comments (3)

Collapse
 
dotallio profile image
Dotallio

Finally! nullptr makes so much sense and should save a ton of headaches. Are you planning to update old codebases or only use it for new projects?

Collapse
 
pauljlucas profile image
Paul J. Lucas

I'd love to do that so I could use nullptr and typeof — except I want to keep my open-source projects as portable as possible. That means not assuming most machines have a compiler installed that supports C23 nor requiring one since that would require that such a compiler be installed. That would just be a stumbling block to using my software. Users don't care about C23 or using the latest and greatest in general; they just want software downloaded to be able to compile and run. If you're a non-root user, then you'd have to pester your sysadmin to install a C23-supporting compiler (or compile your own compiler locally, but that's a pain).

Even compilers that largely support C23 now don't support it by default. For example, even though clang supports C23, you have to specify -std=C23 to activate C23 support. I don't know how long it will take until most compilers support C23 by default, but it might take several years. Compared to the C++ community, the C community moves much slower.

In the meantime, my C code is C11. (C17 is only a minor revision, so there's virtually no difference.)

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

this is extremely impressive and honestly overdue in C, i can still remember all the times NULL tripped up my old projects
you think this will push more C devs toward adopting strict standards or sticking to their own habits