What Is uintptr_t and When to Cast Pointers in C
This article explains the purpose of the uintptr_t type
in the C programming language, how it differs from standard integer
types, and the precise conditions under which casting a pointer to an
integer is considered safe and compliant with the C standard. It also
highlights common pitfalls, portability concerns, and best practices for
performing low-level memory operations.
What Is the uintptr_t
Type?
Introduced in the C99 standard and defined in the
<stdint.h> header, uintptr_t is an
unsigned integer type specifically designed to hold a pointer value. The
C standard guarantees that any valid pointer to void can be
converted to uintptr_t and then converted back to a pointer
to void, with the resulting pointer comparing equal to the
original pointer.
Key characteristics of uintptr_t include:
- Optional by Standard: In strict ISO C,
uintptr_t(and its signed counterpartintptr_t) is an optional type. Platforms where pointers cannot cleanly map into an integer type are not required to provide it. However, virtually all modern architectures (including x86, x86-64, and ARM) implement it. - Guaranteed Width: Unlike
intorlong, which vary across platforms (such as the difference between LLP64 on Windows and LP64 on Linux),uintptr_tis always wide enough to store the bit representation of an object pointer on that specific target platform.
When Is It Safe to Cast a Pointer to an Integer?
Casting a pointer to an integer is inherently low-level and architecture-dependent, but doing so is safe under the following conditions:
1. You Use
uintptr_t or intptr_t
It is safe to cast an object pointer (or void *) to
uintptr_t provided that the type is defined by your
implementation. Casting to types such as int,
unsigned int, or long is unsafe because these
types may be 32 bits wide on systems where pointers are 64 bits wide,
leading to data truncation and undefined behavior.
2. You Perform a Valid Round-Trip Conversion
Converting a pointer to uintptr_t and converting it back
to its original pointer type without modifying the integer value yields
an identical, valid pointer:
#include <stdint.h>
#include <assert.h>
int x = 42;
int *original_ptr = &x;
uintptr_t int_val = (uintptr_t)original_ptr;
int *restored_ptr = (int *)int_val;
assert(original_ptr == restored_ptr); // Safe and defined3. Pointer Tagging and Low-Level Bitmasking
When dynamic memory allocations or data structures are guaranteed to
be aligned to multi-byte boundaries (such as 4- or 8-byte boundaries),
the least significant bits of the address are always zero. It is safe to
cast the pointer to uintptr_t to store metadata flags in
those unused lower bits, provided the tags are masked out before
converting the integer back to a pointer for dereferencing.
4. Pointer Hashing and Address Printing
If you need to generate a numeric hash of a memory address for use in
custom hash tables, converting the pointer to uintptr_t
allows you to apply integer-based hash functions safely. Similarly, it
allows inspecting addresses where printf("%p", ...) cannot
be directly used.
When Is It Unsafe?
- Function Pointers: The C standard only guarantees
that
uintptr_tcan holdvoid *(object pointers). Function pointers may have different sizes or representations on certain architectures. While POSIX systems require function pointers and object pointers to have compatible representations, pure ISO C does not guarantee that a function pointer fits into auintptr_t. - Pointer Arithmetic via Integers: Casting to
uintptr_t, performing arbitrary integer additions or multiplications, and casting back to a pointer to dereference it can violate memory alignment rules, bypass compiler optimization assumptions (strict aliasing and pointer provenance), or access invalid memory, resulting in undefined behavior. - Truncated Integer Types: Assigning a pointer to
uint32_ton a 64-bit platform discards the upper 32 bits, completely invalidating the pointer upon casting back.