C++ convert pointer from one type to another

Viewed 373

I've heard that it is not allowed to cast a pointer from one type to another, but I don't understand why.

For example, in the following code, all I do is reading the casted value, so how can things go wrong, if they can?

struct Foo {
    int a, b, c, d;
};

int main() {
    Foo f = {1, 2, 3, 4};

    int *abcd = *((int *)&f);

    // Now I should be able to access the values like:
    int a = abcd[0];
    int b = abcd[1];
   
    // Then, I only read the values
    // ...

    return 0;
}
3 Answers

You haven't broken strict-aliasing, you never type-punned anything.

What you did violate is pointer arithmetic rules. Pointer arithmetic may only be performed on arrays, doing otherwise is UB. The reason for such rules are all similar, to facilitate some kind of optimization.

void frobnicate(int*);

int bar()
{
    Foo foo = {1, 2, 42, 4};
    frobnicate(&foo.b);
    return foo.c;
}

Is the compiler allowed to just return 42;? Yes, supposedly under rules given by the standard.

But this is also where the standard becomes schizophrenic. There is a particularly awkward macro offsetof, which is the offset of a member to the struct. Under strict reading of the standard, it offers no legal use case: you can't do pointer arithmetic with it.

Incidentally, compilers decided that they do allow pointer arithmetic on structs and disable these kinds optimization. This is in fact required to compile the Linux kernel (C has an analogous rule).

so how can things go wrong, if they can?

Well, for the most part, they can't, as long as you make sure that there are no padding in between.

C++ is derived from C, and the authors of the C89 Standard did not want to forbid a compiler, given something like [their example, quoted verbatim]:

int a;
void f( double * b )
{
  a = 1;
  *b = 2.0;
  g(a); // Presumably g is declared as e.g. `void g(int);` somewhere?
} 

from generating code that passes g the value written to a, rather than having it read a after the write to *b and pass g whatever value was read. Such optimizations are entirely reasonable in cases where an object gets accesses twice, and nothing that a compiler would need to see while processing such accesses would suggest any relationship between the type of the object and any other types which are used between those accesses.

Just as the authors of the Standard saw no need to mandate that implementations be capable of processing any particular nesting of function calls, they saw no need to mandate that compilers expend any particular level of effort recognizing relationships between pointers and lvalues of different types. Given something like:

long get_float_bits( float * b )
{
  return *(long*)b;
} 
float q;
long test(void)
{
  q = 1.0f;
  long result = get_float_bits(&q);
  q = 2.0f;
}

I don't think of the Committee members could have imagined anyone compiler seriously arguing that because get_float_bits couldn't possibly access an object of type float, a quality compiler should omit the first write to q. Such logic would have been seen as similar to a compiler writer deciding that because the Standard doesn't mandate that implementations support more than two levels of function nesting, quality compilers should optimize programs by omitting any calls to functions that they could prove were nested at least four deep. The reason the Standard doesn't prohibit such conduct isn't that the authors wanted to invite it, but rather because they would have viewed such conduct as sufficiently obviously outrageous that nobody would buy compilers that behaved in such fashion.

Unfortunately, the authors of C89 went out of their way to avoid suggesting that some implementations might be viewed as better than others, and thus avoided mentioning anything that quality implementations should do without being absolutely required to do them, and the authors of gcc have interpreted such avoidance as meaning that any code which relied upon anything not mandated by the Standard should be viewed as "broken".

Actually, what are you doing is not a cast. Actual cast can be done in C-style following way:

    int *abcd = (int *)&f;

Also you can cast f to (int *) using reinterpret_cast, in C++ style:

    int *abcd = reinterpret_cast<int *>(&f);
Related