{
  "id": 9134213,
  "title": "Type Punning in C and C++",
  "url": "https://urgent.news/2026/09/22/type-punning-in-c-and-c",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-09-22T11:29:47.000Z",
  "source": {
    "name": "Hacker News",
    "slug": "hacker-news",
    "url": "https://blog.pwkf.org/2026/09/21/correct-type-punning-in-c.html"
  },
  "original_language": "en",
  "account": "I encountered a bug that took considerable time to isolate. The issue stemmed from type punning. Specifically, a pointer cast functioned correctly when compiled at -O0 but malfunctioned at -O2. The distinction between C and C++ in this scenario is hazardous, with many blog posts providing inaccurate information. Type punning involves treating memory as distinct types during reads and writes. It is crucial for serialization, network protocols, and low-level hardware access. However, \"works in practice\" and \"has defined behavior\" are not synonymous. In C, the secure methods for type punning are unions and memcpy. Pointer casts are technically undefined behavior under strict aliasing rules, despite working across all encountered compilers. A union enables writing as one type and reading as another, which is defined behavior in C. Similarly, memcpy is safe and allows the compiler to optimize it into a register move. The latter compiles, runs, and yields the correct answer on every platform. However, this is still undefined behavior. The strict aliasing rule stipulates that an object should only be accessed via an lvalue of its effective type, a qualified version of it, or a character type. Pointer casts to unrelated types breach this rule. In C, types are merely a method of interpreting memory. In C++, types are fundamental entities; the compiler can assume that different types never alias each other. Consequently, with GCC or Clang at -O2, the code returns 2. At -O1 or lower, it returns 0. The compiler perceives u64 as uint64_t* and c as struct c* as different types, assuming they don't alias. Therefore, the second *c-a == 2 check is optimized away based on the assumption that writing *u64 = 4 cannot alter c-a. This is technically correct under the standard, even though the memory types overlap. The more profound explanation is found in \"Taking a Byte Out of C++ - Avoiding Punning by Starting Lifetimes,\" which delves into why C++ adopted this approach. If you're writing C and require type punning, utilize a union or memcpy. Pointer casts \"work\" until they don't. \"Don't\" signifies that the compiler silently optimizes away the code you believe is executing. If you're writing C++, the same applies, plus the compiler possesses greater latitude to disrupt things under the as-if rule. The bug I initially dealt with involved a pointer cast from float* to uint32_t* within a performance-critical loop. At -O2, the loop was optimized under strict aliasing assumptions, and the written values never appeared in their expected locations. A union resolved this issue in ten minutes.",
  "summary": null,
  "key_points": [],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}