C++ language usability enhancements — nullptr and constexpr
A compact guide covering two small but important features that improve code clarity and safety: nullptr (C++11) and constexpr (C++11 and later).
1. nullptr — the modern null pointer literal
nullptr is a keyword (since C++11) that denotes a null pointer literal of type std::nullptr_t.
It converts implicitly to any pointer type and to pointer-to-member types, but it does not convert to integers. This makes code using nullptr safer and clearer than older idioms that used NULL or 0.
Quick summary
- Introduced in C++11
- Type:
std::nullptr_t - Converts to: raw pointer types and pointer-to-member types
- Does not convert to: ordinary integer types
Why prefer nullptr?
NULLis typically a macro expanding to0(an integer). That can cause surprising overload resolution results.nullptrhas its own dedicated type, so it won't accidentally match integer overloads.- Improves code clarity:
nullptrunambiguously means "pointer with no target."
Small contract
- Inputs: none (it's a literal)
- Outputs: a value of type
std::nullptr_tthat converts to pointer types - Error modes: none
- Success: useable in comparisons, assignments, overload resolution and template deduction where a null pointer is required
Examples
Basic overload resolution:
#include <iostream>
#include <type_traits>
void f(int) { std::cout << "f(int)\n"; }
void f(char*) { std::cout << "f(char*)\n"; }
int main() {
// Ambiguous or surprising with 0 or NULL:
// f(0); // calls f(int)
// f(NULL); // may call f(int) depending on how NULL is defined
f(nullptr); // unambiguously calls f(char*)
std::nullptr_t n = nullptr;
static_assert(std::is_same<decltype(n), std::nullptr_t>::value, "type is nullptr_t");
}
Output:
f(char*)
Template deduction example:
template<typename T>
void h(T) { /* ... */ }
int main() {
h(nullptr); // T deduced as std::nullptr_t
}
Pointer-to-member and smart pointers:
struct S { int x; };
int main() {
int S::*pm = nullptr; // pointer-to-member initialized to null
std::unique_ptr<int> up = nullptr;
std::shared_ptr<int> sp(nullptr);
up.reset(); // sets to nullptr
}
Interactions with C and varargs
Be careful passing nullptr to C APIs that use varargs or expect integer types. nullptr is not an integer, so pass explicit integral values when the C API expects them.
Bad example:
// printf expects an int for %d — don't pass nullptr
// printf("%d\n", nullptr); // wrong: type mismatch
printf("%d\n", 0); // correct
If you must bridge to a C API that expects a pointer-sized integer, cast explicitly and document why:
// Only when the API really expects an integer representation of a pointer
void c_api(void* p);
c_api(static_cast<void*>(nullptr));
Pitfalls and notes
- Avoid comparing
nullptrto integers in normal code. Althoughnullptr == 0is allowed (and yields true), comparisons with integers defeat the purpose of using a pointer literal with a dedicated type. sizeof(nullptr)gives the size ofstd::nullptr_t, which is implementation-defined. Prefer relying on types rather than sizes.- In template code, if you want a pointer type deduced, use a signature that requires a pointer (e.g.,
T*) rather than relying onnullptrto force a pointer deduction.
Best practices
- Use
nullptreverywhere you previously usedNULLor0to mean a null pointer. - Prefer function signatures that make pointer intent explicit (e.g., use
T*when you really want a pointer), which plays well withnullptrand template deduction. - When interacting with C or varargs, ensure you pass values of the expected type; use explicit casts if necessary and comment why.
Quick reference
- Keyword:
nullptr - Type:
std::nullptr_t - Introduced: C++11
- Use for: null pointer literals (raw pointers, pointer-to-member, smart pointers initialization)
2. constexpr — compile-time computation and constants
constexpr (since C++11, with many relaxations in later standards) marks functions and variables that can be evaluated at compile time. Well-placed constexpr improves performance, gives stronger guarantees, and documents intent.
Quick summary
- Introduced: C++11 (improved in C++14, C++17, C++20)
- Use for: compile-time constant expressions, functions usable in constant expressions
- Variants:
constexprfunctions/variables,consteval(C++20 immediate function),constinit(C++20 static initialization)
Example — constexpr function (factorial)
#include <array>
#include <iostream>
constexpr int factorial(int n) {
return n <= 1 ? 1 : (n * factorial(n - 1));
}
int main() {
constexpr int f5 = factorial(5); // computed at compile time
std::cout << "5! = " << f5 << '\n';
// Use in contexts requiring compile-time constants
std::array<int, factorial(4)> arr{}; // size is 24
(void)arr;
}
This code compiles with C++11 and later; C++14 relaxed constexpr rules make writing non-trivial constexpr functions easier.
consteval and constinit (C++20)
consteval int make_id() { return 42; } // must be evaluated at compile time
constinit int global_counter = 0; // ensures static initialization, not zero-initialized at runtime
int main() {
constexpr int id = make_id();
(void)id;
}
Use consteval when a function must produce a compile-time value. Use constinit to ensure static variables are initialized at compile time where required.
Pitfalls and notes
- Overusing
constexprcan force more work onto the compiler; apply it where compile-time evaluation is beneficial. - Not all code can be
constexpr— I/O, most dynamic allocations, and certain system interactions must remain runtime-only. - As the language evolves, more constructs become
constexpr-friendly (C++14/17/20), so prefer writing constexpr-capable code when appropriate.
Summary
nullptrimproves pointer semantics and overload clarity; use it in C++11+ code.constexprmoves computation to compile time, documents intent, and enables stronger guarantees; use it where compile-time evaluation is helpful, and prefer the newer features (consteval,constinit) when applicable.