Search code examples
cpointersnullc89

Integer representation(s) of NULL constant: Guaranteed to be unique?


This is a "according to the very words of the C-Standard"-Question. Is it guaranteed, that a cast from NULL to an unsigned integer, big enough to hold any pointer, results in the same value, irrespectively of the type of the pointer, that holds NULL?

Please see the following piece of code, where T1, T2 might be of any type constructable in C:

#include <stddef.h>

/* unsigned int be known to be big enough to hold any object pointer */
unsigned int ui_ptr1, ui_ptr2, ui_ptr3, res;

ui_ptr1 = (unsigned int) NULL;
ui_ptr2 = (unsigned int) (T1 *) NULL;
ui_ptr3 = (unsigned int) (T2 *) NULL;

res = (ui_ptr1 == ui_ptr2) && (ui_ptr1 == ui_ptr3);

Is res guaranteed to be 1 for any T1 and T2? From ISO:9899:1990 (C90) I know, that NULL is equivalent to an implementation defined null-pointer constant (7.1.6). And I know that conversion form a given pointer type, and back again is well defined, but the value is implementation defined. (6.3.4; G.3.7, 2. "-").

But I cannot find any argumentation on the above. Have I missed something? Are there more guarantees in more... recent... C Standard iterations?

Background: I want to transport a pointer between functions via an unsigned integer (I have no choice here) through some static variable, that need to initialized proper by a 3. instance. The initializing instance does not know the type of the pointer to be transported, so it needs to initialize with a integer constant (without casts evolved), that but then represents a/the correct NULL-Pointer constant. If not undefined behavior is lurking, when I try to do anything with it. (see also: Allowed operations on an possibly invalid pointer by the strict interpretation of the C Standard).


Solution

  • From C99 and C11, 6.3.2.3 p3:

    An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. 55) If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

    And p4:

    Conversion of a null pointer to another pointer type yields a null pointer of that type. Any two null pointers shall compare equal.

    (Also, footnote 56/67 remarks that "The mapping functions for converting a pointer to an integer or an integer to a pointer are intended to be consistent with the addressing structure of the execution environment").

    (A draft of C89 has the equivalent text to the C99 quoted above in section 3.2.2.3).

    So, it seems fairly safe to assume, on an architecture with a linear addressing scheme and where the pointer size is equal to the word size, that a single integer value represents NULL - especially if you go through the void * type. In theory though, a NULL pointer (even void *) when converted to an integer might not equal 0, since that's not mandated. You can get around this by always converting through void * before comparing whether the value represents NULL.

    That is, if you change your above declarations of ui_ptr2 and ui_ptr3 to:

    ui_ptr2 = (unsigned int) (void *) (T1 *) NULL;
    ui_ptr3 = (unsigned int) (void *) (T2 *) NULL;
    

    ... then you can at least be sure that each of:

    (void *) ui_ptr2 == (void *) ui_ptr3
    (void *) ui_ptr2 == NULL
    (void *) ui_ptr3 == NULL
    

    ... will be true. If you cast either (void *) ui_ptr2 or (void *) ui_ptr3 to some other pointer type, it should still be NULL, as per 6.3.2.3 p4 quoted above.

    You are, however, relying on the conversion of pointer-to-integer being reversible. The conversion is implementation defined, in both directions, and though one would hope that converting a pointer to an integer and then back to a pointer yields the original pointer value, that's not strictly mandated (though footnote 56 quoted above strongly recommends it by implication). Furthermore there is no guarantee that all possible pointer values are representable in any integer type (6.3.2.3 p6). Your only real way around this is to verify that sizeof(int) >= sizeof(void *) and then use memcpy to transfer the representation of the pointer to the integer, and back, in which case there is no longer a need to go via void * (just so long as the type of the pointer you store is the same as the type you retrieve).