Why is type punning considered UB?

穿精又带淫゛_ 提交于 2021-01-04 03:14:25

问题


Imagine this:

uint64_t x = *(uint64_t *)((unsigned char[8]){'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'});

I have read that type puns like that are undefined behavior. Why? I am literally, reinterpreting 8 bytes of bytes into an 8 byte integer. I don't see how that's different from a union except the type pun being undefined behavior and unions not being? I asked a fellow programmer in person and they said that if you're doing it, either you know what you're doing very well, or you're making a mistake. But the community says that this practice should ALWAYS be avoided? Why?


回答1:


Ultimately the why is "because the language specification says so". You don't get to argue with that. If that's the way the language is, it's the way it is.

If you want to know the motivation for making it that way, it's that the original C language lacked any way of expressing that two lvalues can't alias one another (and the modern language's restrict keyword is still barely understood by most users of the language). Being unable to assume two lvalues can't alias means the compiler can't reorder loads and stores, and must actually perform loads and stores from/to memory for every access to an object, rather than keeping values in registers, unless it knows the object's address has never been taken.

C's type-based aliasing rules somewhat mitigate this situation, by letting the compiler assume lvalues with different types don't alias.

Note also that in your example, there's not only type-punning but misalignment. The unsigned char array has no inherent alignment, so accessing a uint64_t at that address would be an alignment error (UB for another reason) independent of any aliasing rules.




回答2:


Type punning is considered UB because the authors of the Standard expected that quality implementations intended for various purposes would behave "in a documented manner characteristic of the environment" in cases where the Standard imposed no requirements, but where such behavior would serve the intended purposes. As such, it was more important to avoid imposing overly strong mandates on implementations than to require that they support everything programmers would need.

To adapt and slightly extend the example from the Rationale, consider the code (assume for simplicity a commonplace 32-bit implementation):

unsigned x;
unsigned evil(double *p)
{
  if (x) *p = 1.0;
  return x;
}
...
unsigned y;
int main(void)
{
  if (&y == &x + 1)
  {
    unsigned res;
    x=1;
    res = evil((double*)&x);
    printf("You get to find out the first word of 1.0; it's %08X.\n", res);
  }
  else
  {
    printf("You don't get to find out the first word of 1.0; too bad.\n");
  }
  return 0;
} 

In the absence of the "strict aliasing rule", a compiler processing evil would have to allow for the possibility that it might be invoked as shown in test on an implementation which might happen place two int values consecutively in such a way that a double could fit in the space occupied thereby. The authors of the Rationale recognized that if a compiler returned the value of x that had been seen by the if, the result would be "incorrect" in such a scenario, but even most advocates of type punning would admit that a compiler that did so (in cases like that) would often be more useful than one that reloaded x (and thus generated less efficient code).

Note that the rules as written aren't describe all cases where implementations should support type punning. Given something like:

union ublob {uint16_t hh[8]; uint32_t ww[4]; } u;

int test1(int i, int j)
{
  if (u.hh[i])
    u.ww[j] = 1;
  return u.hh[i];
}

int test2(int i, int j)
{
  if (*(u.hh+i))
    *(u.ww+j) = 1;
  return *(u.hh+i);
}

int test3(int i, int j)
{
  uint16_t temp;
  {
    uint16_t *p1 = u.hh+i;
    temp = *p1;
  }
  if (temp)
  {
    uint32_t *p2 = u.ww+j;
    *p2 = 1;
  }
  {
    uint16_t *p3 = u.hh+i;
    temp = *p3;
  }
  return temp;
}

static int test4a(uint16_t *p1, uint32_t *p2)
{
  if (*p1)
    *p2 = 1;
  return *p1;
}
int test4(int i, int j)
{
  return test4a(u.hh+i, u.ww+j);
}

Nothing in the Standard, as written, would imply that any of those would have defined behavior unless they all do, but the ability to have arrays within unions would be rather useless if test1 didn't have defined behavior on platforms that support the types in question. If compiler writers recognized that support for common type punning constructs was a Quality of Implementation issue, they would recognize that there would be little excuse for an implementation failing to handle the first three, since any compiler that isn't deliberately blind would see evidence that the pointers were all related to objects of common type union ublob, without feeling obligated to handle such possibilities in test4a where no such evidence would exist.



来源:https://stackoverflow.com/questions/63422076/why-is-type-punning-considered-ub

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!