Why C++ programmers don't use NULL?
As most Arduino users, you probably learned the C language before C++. Of
course, you did! It’s the way everybody learns C++ today. While learning C, you
applied the good practice of using a NULL
to designate an empty pointer.
What if I tell you that using NULL
is considered as a code smell for
experienced C++ developers? And, what if I tell you that most C++ coding
standards now forbids using NULL
entirely?
Read on if you want to know why!
Why do we use NULL in the first place?
Very often, we need a way to express that a variable contains no value. In a C
program, we do that by declaring a pointer and make it refer to a special
address that can never point to something real: zero. For instance, we could
declare an empty color
string like that:
const char* color = 0;
But that wouldn’t be very clear, would it? That’s why C programmers insist on
using the symbol NULL
to identify an empty pointer clearly. So, to follow best
practices, we should write:
const char* color = NULL;
As long as we stay in the realm of the C language, everything is fine and you
can (and should) still use NULL
.
What most people think NULL is?
I believe most people think that NULL
is defined like that:
#define NULL ((void*)0)
Indeed, it is the right definition for the C language. It works because the
language implicit converts void*
to T*
for any type T
.
C++, however, with its strong-typing philosophy, doesn’t implicitly convert
from void*
to T*
.
Let’s try:
#define NULL ((void*)0)
const char* color = NULL;
A C compiler accepts these two lines without any problem, but a C++ compiler returns the following error:
error: invalid conversion from 'void*' to 'const char*' [-fpermissive]
To work around this error, we would have to cast NULL
explicitly:
#define NULL ((void*)0)
const char* color = reinterpret_cast<const char*>(NULL);
Ugly, isn’t it?
What NULL really is?
We saw that the definition of NULL
in C++ could not be the same as the
definition in C. So, what is it?
Well, it is very likely to be defined like that:
#define NULL 0
I know, it’s quite disappointing… The C++ standard allows other definitions
but, from my experience, it’s often defined to 0
.
Why does that make a difference?
OK, NULL
is not a void*
but an int
, so what? No big deal!
Well, maybe not a big deal, but still very annoying in certain situations.
For example:
const char *color = NULL;
String colorStr1(color);
String colorStr2(NULL);
At first sight, colorStr1
and colorStr2
look identical. So, if we compare
them, they should be equal, right?
if (colorStr1== colorStr2) {
Serial.println("colorStr1 == colorStr2");
} else {
Serial.println("colorStr1 != colorStr2");
}
If you run this program, it displays colorStr1 != colorStr2
, proving that the
String
s differ.
Let’s print them:
Serial.print("colorStr1 = ");
Serial.println(colorStr1);
Serial.print("colorStr2 = ");
Serial.println(colorStr2);
These four line produces the following output:
colorStr1 =
colorStr2 = 0
Strange, isn’t it?
Problem 1: NULL messes with functions overloading
We saw that the definition of NULL
exposes an inconsistency in the String
class.
To understand the reason, let’s look at a simpler version of the same problem.
We declare two functions, like that:
void f(const char*);
void f(int);
Since these functions have the same name, they are overloads, meaning that the compiler calls one or the other depending on the type of the argument. Remember that function overloading is available in C++, but not in C.
So, guess which overload is chosen when we call:
f(NULL);
If you’re like me, your first deduction was that this line should call the
overload taking a char-pointer. However, after what we saw earlier, you should
understand why it calls the other overload. Indeed, as far as the compiler is
concerned, it’s identical to calling f()
like that:
f(0);
How is that related to the String
problem? Simple! By passing NULL
to the
constructor, we just called the wrong overload, the one that takes an integer
and converts it to a string.
Problem 2: NULL messes with template type deduction
We saw that the strange definition of NULL
pushes the compiler to select the
wrong overload but is it the only problem? Unfortunately no, it also confuses
the template type deduction. Imaging we declare this template function:
template<typename T>
void printValue(T value);
Then, we specialize it for integer and pointer:
template <>
void printValue<int>(int value) {
Serial.print("Integer: ");
Serial.println(value, DEC);
}
template <>
void printValue<void *>(void *value) {
Serial.print("Pointer: 0x");
Serial.println((intptr_t)value, HEX);
}
Now, if we call printValue(NULL)
, it will print:
Integer: 0
I know there are better ways to solve this problem; I show you this example
because it illustrates the problem with NULL
. With this program, we see that
the compiler deduces the template type to be an integer, while we imagined it
would be some pointer type.
Let’s see how this type deduction thing can affect your program on the real life. Imaging you use ArduinoJson 5, and you write something like that:
DynamicJsonBuffer jb;
JsonObject& obj= jb.parseObject("{\"id\":0}");
if (obj["id"] == NULL) {
Serial.println("ERROR: id is missing");
}
This program displays an error, even if the key id
is present in the object,
because the compiler believes you wrote:
if (obj["id"] == 0) {
So, you see, this template type deduction can have real implications.
By the way, if you want to fix this program, see JsonObject::containsKey().
The solution
I told you that C++ programmers banned NULL
from their code-base, but what do
they use instead?
Instead of NULL
, they use nullptr
, a new keyword introduced in C++11.
- Like
NULL
,nullptr
implicitly converts toT*
for any typeT
. - Unlike
NULL
,nullptr
is not an integer so it cannot call the wrong overload. - Unlike
NULL
,nullptr
has its own type,nullptr_t
, so the compiler makes correct type deductions.
Are there any drawbacks to using nullptr
instead of NULL
? No, unless you
target old compilers that don’t support C++11, which is very unlikely.
Mitigations
Before wrapping up, I’d like to clarify something I skipped to simplify this article.
The C++ standard committee knows there is a lot of existing code using NULL
that would become safer if it was using nullptr
instead. So, in the C++11
standard, they allowed the definition of NULL
to be:
// either
#define NULL 0
// or
#define NULL nullptr
// both are legal in C++11
From my experience, the standard library defines NULL
as 0
, not as
nullptr
. However, what the C++11 standard allows us it to replace it in our
code-base to make our old code safer with a single line in the right header.
Compiler writers are also well aware of the problems with NULL
, so they
implemented a special case allowing them to issue a warning when appropriate.
For example, when we call f(NULL)
as we did earlier, the compiler produces
the following warning:
warning: passing NULL to non-pointer argument 1 of 'void f(int, int)' [-Wconversion-null]
You always look at the warnings, right?
Conclusion
I could have written this article with just one single line:
Don’t use
NULL
, usenullptr
instead
That’s not the way I work. I don’t just tell people what to do; I explain why they should do it. Understanding the rationale behind the language features is what will make you a great C++ developer, so stay tuned to read more articles like this one.
By the way, if you want to practice at home, download the samples for this article on github.com/bblanchon/cpp4arduino.
See you soon!