Does C++ guarantee consistent pointer representations?

Does C++ guarantee consistent pointer representations when a pointer is casted to other pointer types?

For example, does C++ guarantee anything about the following program?

<stdint.h>

struct Foo {};
struct Bar : Foo {};

int main() {
    Bar obj;

    Foo * a = &obj;
    Bar * b = &obj;
    void *c = &obj;
#if MAYBE_UB
    int * d = reinterpret_cast<int *>(&obj);
#endif

    auto aa = reinterpret_cast<uintptr_t>(a);
    auto bb = reinterpret_cast<uintptr_t>(b);
    auto cc = reinterpret_cast<uintptr_t>(c);
#if MAYBE_UB
    auto dd = reinterpret_cast<uintptr_t>(d); // UB? Not reading the pointee...
#endif

    if (aa != bb) printf("bb differsn");
    if (aa != cc) printf("cc differsn");
#if MAYBE_UB
    if (aa != dd) printf("dd differsn");
#endif

    return 0;
}

Answer

Does C++ guarantee consistent pointer representations?

No, pointers can be represented in any inconsistent way compiler wants them to. Generally, the language tries to be abstract and talk as little as possible about representation of stuff. It’s left for the implementation to figure that out.

Does C++ guarantee consistent pointer representations when a pointer is casted to other pointer types?

No, pointers can be represented in any way they want. Using rocks and sticks, for example.

does C++ guarantee anything about the following program?

TL;DR Well, yes, that the code should compile (ignoring the missing #includfe on top). But there is no guarantee about the output.

Aaanyway, I do not think you are interested in the representation of pointers, but if the value of pointer changes when the pointer value is converted to a different type. This has nothing to do with how the value is “represented”, the representation of the pointer can change anyhow the compiler wants to. Well, ok, we know char * and void * have the same representation, from https://eel.is/c++draft/basic.compound#5 :

A pointer to cv void can be used to point to objects of unknown type. Such a pointer shall be able to hold any object pointer. An object of type “pointer to cv void” shall have the same representation and alignment requirements as an object of type “pointer to cv char”.

Anyway, we know about void * pointer https://eel.is/c++draft/expr#conv.ptr :

A prvalue of type “pointer to cv T”, where T is an object type, can be converted to a prvalue of type “pointer to cv void”. The pointer value is unchanged by this conversion.

And this also is relevant:

A prvalue of type “pointer to cv D”, where D is a complete class type, can be converted to a prvalue of type “pointer to cv B”, where B is a base class ([class.derived]) of D. If B is an inaccessible ([class.access]) or ambiguous ([class.member.lookup]) base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion is a pointer to the base class subobject of the derived class object.

But there is nothing about the value of the result. It can be the same, it can be different, it’s not specified.

The problem arises that your code uses uintptr_t. We know that cstdint has to be the same as in C stdint.h, and from C we know only that C99 7.18.1.4:

The following type designates an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:

   uintptr_t

We know nothing about the value of (uintptr_t)(Bar*)&obj. We also do not know if two uintptr_t variables compare equal, even when they were created from two equal void pointers. Only that they compare equal when you convert them back. We fall back to general rule at reinterpret_cast https://eel.is/c++draft/expr#reinterpret.cast-4 :

A pointer can be explicitly converted to any integral type large enough to hold all values of its type. The mapping function is implementation-defined.

[Note 2: It is intended to be unsurprising to those who know the addressing structure of the underlying machine. — end note]

The “mapping function” can be anything. It can produce inconsistent values that depend on anything – it’s implementation defined. So, basically, we know nothing about the resulting uintptr_t values, except there should be some values there.

Anyway, from the above, I conclude that all uintptr_t conversions in your code result in an implementation defined value and can have any value the implementation wants them to have. The program has defined, but implementation-defined behavior – any of those ifs can be true or false, depending on the “pointers to integers mapping function” the compiler uses.

However, we know, that any sane compiler will have a sane “mapping function” that maps pointers to integral types, and this mapping functions will be “unsurprising to those who know the addressing structure of the underlying machine” and will produce consistent values. You might be also interested in this pointer provenance.

As for MAYBE_UB, there is nothing undefined in it, we know that https://eel.is/c++draft/expr#reinterpret.cast-7 :

An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue v of object pointer type is converted to the object pointer type “pointer to cv T”, the result is static_­cast<cv T*>(static_­cast<cv void*>(v)).

But we know from https://eel.is/c++draft/expr#static.cast-13 :

A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T”, where T is an object type and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.

The resulting pointer can be unspecified or unchanged, depending on if int satisfies alignment requirements of &obj, but the code is still defined.