Low-Level Memory

reinterpret_cast and Low-Level Pointer Conversions

reinterpret_cast allows a programmer to take a pointer of one type and tell the compiler to treat the same memory location as a pointer of a different, unrelated type. It also allows a programmer to cast an integer to a pointer, or a pointer to an integer. The language imposes very little safety checking on the operation, so it is the programmer's responsibility to know the cast is valid.

Below is a full program that casts an int pointer to a double one.

#include <iostream>

int main()
{
    int x{42};
    // Pass this resource object's address to the constructor
    int* pi{&x};
    double* pd{reinterpret_cast<double*>(pi)};
    std::cout << *pd << '\n';
}

The object x is a normal int and contains 42. The variable pi gets the address of x, so pi is a valid int*. The line of code reinterpret_cast<double*>(pi) applies the casts that it describes to the value of pi, resulting in a double* value being stored in pd.

When *pd is computed, the CPU will fetch the bits which formerly were the int x, but reinterprets those bits as a double.

Since the bit pattern for the integer 42 is nonsensical when reinterpreted as a double on nearly all architectures, the code prints some nonsensical value. The program is permitted to crash, produce a Quiet NaN, or any other number; the behavior is undefined.

The conversion also works in reverse with identical rules. Converting from a double* to an int* again is allowed within the language. But the same dangers are present if the data in memory was never a double. The only thing guaranteed by the standard is if the value of a pointer is converted to a pointer to another type, then to convert it back to a pointer to the original type, the value is required to compare equal to the original value; the intermediate value could not be used to access the data in memory.

The reinterpret_cast is also the operation that takes an int into a pointer and a pointer into an int.

#include <cstdint>
#include <iostream>

int main()
{
    int x{10};
    // Reinterpret the value as the requested type
    std::uintptr_t addr{reinterpret_cast<std::uintptr_t>(&x)};
    int* p{reinterpret_cast<int*>(addr)};
    std::cout << *p << '\n';
}

The expression &x creates a pointer to the int x. Immediately, this pointer is cast to the integer type std::uintptr_t and stored in addr. Later, the same integer value is cast back to int* and stored in p.

Since the integer never changed and was converted directly back, p is required to point at the original object x and so dereferencing it makes sense and prints 10.

If any arithmetic is done on addr, or the integer value is obtained from elsewhere, the resulting pointer can point anywhere, making dereferencing it undefined.

A common real world application of these conversions is when a library returns an opaque handle as a void*, or as an int, and client code must convert the handle back to a typed pointer. The conversion again involves reinterpret_cast. However, the programmer already knows, from reading the library documentation, that the bit pattern really does represent a pointer to the required type. However, there is no checking by the compiler.

Because it only changes the type interpretation and leaves the bits in memory unchanged, programmers have also used it as a type punning technique to alias the same memory through two different type interpretations. This violates strict aliasing rules. The language only allows a narrow set of type punning techniques using unions or std::bit_cast to do this safely, but a reinterpret_cast followed by a dereference of the new type is not one of them. As soon as the program performs a read or write through this new pointer while a different object type still resides at that address, the program has left the standard and the optimizer can assume that the two pointers never alias.

Alignment constraints persist after casting. If the address of the original object already met the alignment requirements of the new type, the casted pointer can be used. If the address was not aligned properly for the new type, any dereference of the pointer is undefined behavior. For instance, if on a certain platform, a double requires more strict alignment than an int, then converting an int* that is only four bytes aligned to a double* results in a pointer that cannot safely be dereferenced.

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.