What Exactly Nullptr Is in C++?
In this article, we discuss some basics behind nullptr in C++, including what it is, why it's useful, and when we can use it.
Join the DZone community and get the full member experience.
Join For FreeThe answer to "What exactly nullptr is in C++?" would be a piece of cake for experienced C++ programmers and for those who are aware of Modern C++ (i.e. keyword). But nullptr
is more than just a keyword in C++, and to explain that, I have written this article. But, before I jump into it, we will see issues with NULL
. Then, we'll dive into the unsophisticated implementation of nullptr
and some use-cases of nullptr
.
Why Do We Need Nullptr?
To distinguish between an integer, 0 (zero), i.e. NULL, and an actual null of type pointer.
Nullptr vs NULL
NULL
is0
(zero) i.e. integer constant zero with C-style typecast tovoid*
, whilenullptr
is prvalue of typenullptr_t
, which is an integer literal that evaluates to zero.- For those of you who believe that
NULL
is the same i.e.(void*)0
in C and C++. I would like to clarify that no it's not:- NULL - cppreference.com (C).
- NULL - cppreference.com (C++)
- C++ requires that macro
NULL
be defined as an integral constant expression, having the value of0
. So, unlike in C,NULL
cannot be defined as(void *)0
in the C++ standard library.
Issues With NULL
Implicit Conversion
xxxxxxxxxx
char *str = NULL; // Implicit conversion from void * to char *
int i = NULL; // OK, but `i` is not pointer type
Function Calling Ambiguity
xxxxxxxxxx
void func(int) {}
void func(int*){}
void func(bool){}
func(NULL); // Which one to call?
Compilation produces the following error:
xxxxxxxxxx
error: call to 'func' is ambiguous func(NULL); ^~~~
note: candidate function void func(bool){} ^
note: candidate function void func(int*){} ^
note: candidate function void func(int){} ^
1 error generated.
compiler exit status 1
Constructor Overload
xxxxxxxxxx
struct String
{ String(uint32_t) { /* size of string */ } String(const char*) { /* string */ }
};
String s1( NULL );
String s2( 5 );
In such cases, you need explicit cast (i.e., String s((char*)0))
.
Implementation of Unsophisticated Nullptr
nullptr
is a subtle example of the Return Type Resolver idiom that automatically deduces a null pointer of the correct type depending upon the type of the instance it is assigning to.
Consider the following simplest and most unsophisticated nullptr
implementation:
xxxxxxxxxx
struct nullptr_t { void operator&() const = delete; // Can't take address of nullptr
template<class T> inline operator T*() const { return 0; }
template<class C, class T> inline operator T C::*() const { return 0; }
};
nullptr_t nullptr;
If the above code seems strange and weird to you, then I would suggest you go through my earlier article on advanced C++ concepts. The magic here is just the templatized conversion operator.
If you are into a more authoritative source, then, here is a concrete implementation of nullptr from LLVM header.
Use-Cases of Nullptr
xxxxxxxxxx
struct C { void func(); };
int main(void)
{ int *ptr = nullptr; // OK void (C::*method_ptr)() = nullptr; // OK nullptr_t n1, n2; n1 = n2; //nullptr_t *null = &n1; // Address can't be taken.
}
As shown in the above example, when nullptr
is being assigned to an integer pointer, a int
type instantiation of the templatized conversion function is created. And same goes for method pointers too.
This way, by leveraging template functionality, we are actually creating the appropriate type of null pointer every time we do a new type assignment.
As nullptr
is an integer literal with value zero, you can not able to use its address, which we accomplished by deleting and operator.
Function Calling Clarity With Nullptr
xxxxxxxxxx
void func(int) { /* ... */}
void func(int *) { /* ... */}
void func(bool) { /* ... */}
func(nullptr);
Now, func( int* )
will be called as nullptr
will implicitly be deduced to int*
.
Typecasting on Nullptr_t
A cast of nullptr_t
to an integral type needs a reinterpret_cast
and has the same semantics as a cast of (void*)0
to an integral type.
Casting nullptr_t
to an integral type holds true as long as destination type is large enough. Consider this:
xxxxxxxxxx
// int ptr_not_ok = reinterpret_cast<int>(nullptr); // Not OK
long ptr_ok = reinterpret_cast<long long>(nullptr); // OK
A reinterpret_cast
cannot convert nullptr_t
to any pointer type. Use static_cast
instead.
xxxxxxxxxx
void func(int*) { /*...*/ }
void func(double*) { /*...*/ }
func(nullptr); // compilation error, ambiguous call!
// func(reinterpret_cast<int*>(nullptr)); // error: invalid cast from type 'std::nullptr_t' to type 'int*'
func(static_cast<int*>(nullptr)); // OK
nullptr
is implicitly convertible to any pointer type so explicit conversion with static_cast
is only valid.
Nullptr_t Is Comparable
xxxxxxxxxx
int *ptr = nullptr;
if (ptr == 0); // OK
if (ptr <= nullptr); // OK
int a = 0;
if (a == nullptr); // error: invalid operands of types 'int' and 'std::nullptr_t' to binary 'operator=='
From Wikipedia: - …null pointer constant:
nullptr
. It is of typenullptr_t
, which is implicitly convertible and comparable to any pointer type or pointer-to-member type.
- It is not implicitly convertible or comparable to integral types, except forbool
.
xxxxxxxxxx
const int a = 0;
if (a == nullptr); // OK
const int b = 5;
if (b == nullptr); // error: invalid operands of types 'const int' and 'std::nullptr_t' to binary 'operator=='
Template-Argument Is of Type std::nullptr_t
xxxxxxxxxx
template <typename T>
void ptr_func(T *t) {}
ptr_func(nullptr); // Can not deduce T
As discussed earlier, Return Type Resolver needs an assignee to deduce the type.
xxxxxxxxxx
template <typename T>
void val_func(T t) {}
val_func(nullptr); // deduces T = nullptr_t
val_func((int*)nullptr); // deduces T = int*, prefer static_cast though
Conversion to Bool From Nullptr_t
From cppreference:
In the context of a direct-initialization, abool
object may be initialized from a prvalue of typestd::nullptr_t
, includingnullptr
. The resulting value is false. However, this is not considered to be an implicit conversion.
The conversion is only allowed for direct-initialization — not copy-intialization, which includes the case for passing an argument to a function by value. e.g.
xxxxxxxxxx
bool b1 = nullptr; // Not OK
bool b2 {nullptr}; // OK
void func(bool){}
func(nullptr); // Not OK, need to do func(static_cast<bool>(nullptr));
Misc
xxxxxxxxxx
typeid(nullptr); // OK
throw nullptr; // OK
char *ptr = expr ? nullptr : nullptr; // OK
// char *ptr1 = expr ? 0 : nullptr; // Not OK, types are not compatible
static_assert(sizeof(NULL) == sizeof(nullptr_t));
Summary by FAQs
When was nullptr
introduced?
C++11
Is nullptr
a keyword or an instance of a type std::nullptr_t
?
Both true
and false
are keywords and literals, as they have a type ( bool
). nullptr
is a pointer literal of type std::nullptr_t
, and it's a prvalue (i.e. pure rvalue, you cannot take the address of it using &
). For more.
What are the advantages of using nullptr?
- No function calling ambiguity between overload sets.
- You can do template specialization with
nullptr_t
. - Code will become more safe, intuitive, and expressive.
if (ptr == nullptr);
rather thanif (ptr == 0);
.
Is NULL
in C++ equal to nullptr
from C++11?
Not at all. The following line does not even compile:cout<<is_same_v<nullptr, NULL><<endl;
Can I convert nullptr
to bool?
Yes. But only if you use direct-initialization. i.e. bool is_false{nullptr};
. Otherwise, you need to use static_cast
.
How is nullptr
defined?
It's just the templatized conversion operator known as Return Type Resolver.
References
You can find similar resources here, here, and in nullptr proposal(N2431); however, this post will walk you through the ins and outs of the spec step-by-step in a more friendly way so that you come away with a full understanding of the concept without any needless confusion
Published at DZone with permission of Vishal Chovatiya. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments