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 counterpart intptr_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 int or long, which vary across platforms (such as the difference between LLP64 on Windows and LP64 on Linux), uintptr_t is 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 defined

3. 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_t can hold void * (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 a uintptr_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_t on a 64-bit platform discards the upper 32 bits, completely invalidating the pointer upon casting back.