3

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;
}
Jojolatino
  • 696
  • 5
  • 11
  • 3
    No sane compiler is going to do it, but it's totally allowed to change `int a, b, c, d;` into `int a, padding1, b, padding2, c, padding3, d, padding4;` and then your `int b = abcd[1];` is reading indeterminate data. *boom* – NathanOliver Aug 25 '20 at 21:03
  • *"how can things go wrong"* - This example could do completely different things depending on your compiler, the architecture you are running on, how things are laid out in memory, etc. – 0x5453 Aug 25 '20 at 21:05
  • 1
    [Discussion of the problem from the perspective of the C programming language](https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule). C++ is even more paranoid. – user4581301 Aug 25 '20 at 21:07
  • @user4581301: “If everyone is out to get you paranoia is only good thinking” - Dr. Johnny Fever – Fred Larson Aug 25 '20 at 21:37
  • 1
    One way things could go wrong is having the wrong level of indirection so you end up initializing a `int*` variable with an `int` expression, causing a compile error. – Fred Larson Aug 25 '20 at 21:43
  • 1
    @FredLarson sometimes it feels like the entire C++ language is out to get me. – user4581301 Aug 25 '20 at 22:34
  • I believe there's also the possibility that `int a = abcd[0]` and `int b = abcd[-1];` and `int c = abcd[-2];`. I don't think there's a guarantee which way the pointers go. – Mooing Duck Aug 26 '20 at 00:20
  • Does this answer your question? ["dereferencing type-punned pointer will break strict-aliasing rules" warning](https://stackoverflow.com/questions/4163126/dereferencing-type-punned-pointer-will-break-strict-aliasing-rules-warning) – Davis Herring Aug 26 '20 at 02:56
  • Even if your code works as expected, this kind of conversion shouldn't be used. Because struct has the padding mechanism, and in c++, struct can hide some virtual table, so `abcd[n]` can be something unexpected. – Yves Aug 26 '20 at 03:53
  • @user4581301: The Standards were written to deliberately allow implementations specialized for particular purposes to behave in ways that would make them unsuitable for most others, on the presumption that compiler writers would seek to make their products suitable for whatever their users would be trying to do. The problem is not with the language the Standards were written to describe, but rather with compiler writers' failures to recognize that the Standard's failures to forbid compilers from behaving uselessly in various cases aren't meant to suggest that compilers shouldn't... – supercat Aug 28 '20 at 17:59
  • ...be expected to process such cases usefully except when they would have a good reason to do otherwise. Unfortunately, some compiler writers view the fact that the Standard allows nonsensical behavior as being adequate reason for behaving nonsensically. – supercat Aug 28 '20 at 17:59
  • @Yves: In C++, some structure types are allowed to have such compiler-private storage while others aren't. IMHO, Stroussap's making `class` essentially synonymous with `struct`, rather than specifying that a `struct` can't do things that would require compiler-private storage, but is guaranteed not to have any, while any `class` may have such storage, ended up creating needless complexities downstream because there are two types of struct/class, but no nice linguistic way of distinguishing them. – supercat Aug 28 '20 at 18:04

3 Answers3

0

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.

Passer By
  • 19,325
  • 6
  • 49
  • 96
  • On what basis could a compiler assume that `frobnicate` wouldn't convert the passed-in pointer to a `Foo` and access it that way? Useful optimizations could be facilitated if there were a qualifier that would promise not to do such a thing, just as it could be facilitated if there were a qualifier that would promise that a function won't cast away the const-ness of a passed-in pointer type and use it to modify its target, but I don't htink any such qualifiers presently exist, and the optimizations would not be legitimate in their absence. – supercat Aug 28 '20 at 18:06
  • @supercat The original was a bad example. I'm not saying these rules are great, I don't see the point in most of them, I'm just saying what I assume is the reason. Then again I'm not a compiler writer. – Passer By Aug 29 '20 at 08:51
  • In most cases where the behavior of a construct would be implied by storage layouts in the absence of any rule characterizing it as "Undefined Behavior", the authors of the Standard expected that quality implementations intended for various purposes would give the behavioral specifications precedence over "undefinedness" in cases that would be useful for those purposes, without regard for whether the Standard required that they do so. They thus saw no need to to forbid compilers from behaving in ways that might make them unsuitable for many purposes, and thus no need to write rules precisely. – supercat Aug 29 '20 at 16:42
  • @supercat I think we had this debate _way_ too many times (2 years ago). You tend to think the standard should apply pragmatically, I think the standard should be interpreted to the word and acknowledge that it's not perfect. Let's just leave it at that. – Passer By Aug 29 '20 at 17:22
0

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".

supercat
  • 77,689
  • 9
  • 166
  • 211
-1

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);
Enrico
  • 120
  • 5