Home > Back-end >  Is casting a pointer to an incompatible type then casting it back before dereferencing defined behav
Is casting a pointer to an incompatible type then casting it back before dereferencing defined behav

Time:08-09

I realize there are lots of "Is this pointer madness defined behavior in C?" questions on this site, but none of the ones I found seemed to be on point, so if this is a duplicate I apologize.

Let's say I have two struct types Foo and Bar, with Foo having some field int baz. They have the same alignment, but one isn't an extension of the other, so casting a pointer to one to a pointer to the other and dereferencing would definitely be undefined. However, if I cast that pointer back and then dereference it, would that be an issue? I realize this isn't a good idea, but assuming for the moment I have to do it anyways, is the following code defined behavior?

int Get_Baz_With_Weird_Type_Stuff(Foo* fooPtr) {
    Bar* barPtr;
    Foo* fooPtr2;

    barPtr = (Bar*) fooPtr;
    fooPtr2 = (Foo*) barPtr;

    return fooPtr2->baz;
}

Another use case I need to know is casting through function arguments like this

int Get_Baz_But_With_Weird_Type_Stuff(Bar* barPtr) {
    Foo* fooPtr = barPtr;

    return fooPtr->baz;
}

int Get_Value(Bar* bar, int (*callback)(Bar*)) {
    return callback(bar);
}

int Get_Baz_In_Overly_Complicated_Way_For_Some_Reason(Foo* foo) {
    return Get_Value((Bar*) foo, &Get_Baz_But_With_Weird_Type_Stuff);
}

CodePudding user response:

Yes, casting from one type to another and back via an explicit cast is well defined.

From section 3.3.4 of the C89 standard regarding the cast operator:

Conversions that involve pointers (other than as permitted by the constraints of 3.3.16.1) shall be specified by means of an explicit cast; they have implementation-defined aspects: A pointer may be converted to an integral type. The size of integer required and the result are implementation-defined. If the space provided is not long enough, the behavior is undefined. An arbitrary integer may be converted to a pointer. The result is implementation-defined. A pointer to an object or incomplete type may be converted to a pointer to a different object type or a different incomplete type. The resulting pointer might not be valid if it is improperly aligned for the type pointed to. It is guaranteed, however, that a pointer to an object of a given alignment may be converted to a pointer to an object of the same alignment or a less strict alignment and back again; the result shall compare equal to the original pointer. (An object that has character type has the least strict alignment.) A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function that has a type that is not compatible with the type of the called function, the behavior is undefined.

Starting with C99, this verbiage was moved to section 6.3.2.3 (3.2.2.3 in C89) on pointer conversions.

So looking at your first case:

barPtr = (Bar*) fooPtr;
fooPtr2 = (Foo*) barPtr;

Assuming compatible alignment, this is fine.

The second case going through function arguments is mostly fine except for this line:

Foo* fooPtr = barPtr;

Where you're not using an explicit cast as in the first example. If you add a cast:

Foo* fooPtr = (Foo *)barPtr;

Then it's well defined.

CodePudding user response:

Such casts are only well defined if the address in question satisfies the alignment requirements of all intermediate types.

Consider the function:

void copyUnsigned(unsigned *dest, unsigned *src)
{
    memcpy(dest, src, sizeof (unsigned));    
}

When targeting many low-end ARM platforms, the most efficient code to copy four bytes with arbitrarily alignment will take several times as long to execute as the most efficient code to handle cases where source and destination are known to be word aligned. While gcc will process the above using a call to an external memcpy function, clang will generate code that simply performs a single 32-bit load and 32-bit store if it can tell that the pointer types would not support unaligned values. Although memcpy would normally have defined behavior regardless of whether the operands were defined, clang's generated code for the Cortex-M0 will fail if the operands aren't aligned.

  • Related