11

Can you propose at least 1 scenario where there is a substantial difference between

union {
T var_1;
U var_2;
}

and

var_2 = reinterpret_cast<U> (var_1)

?

The more i think about this, the more they look like the same thing to me, at least from a practical viewpoint.

One difference that I found is that while the union size is big as the biggest data type in terms of size, the reinterpret_cast as described in this post can lead to a truncation, so the plain old C-style union is even safer than a newer C++ casting.

Can you outline the differences between this 2 ?

timrau
  • 22,578
  • 4
  • 51
  • 64
user2485710
  • 9,451
  • 13
  • 58
  • 102
  • 2
    As far as I know, using `union`s for type punning is safe in C - I'm not sure about C++, maybe it is not, and then you must use typecasting. –  Jul 29 '13 at 12:18
  • 5
    @H2CO3 I don't know (and honestly don't care) if using unions is safe, but reinterpret_cast is strictly not safer. – R. Martinho Fernandes Jul 29 '13 at 12:19
  • @H2CO3 well, considering the chance of truncation, I will use union anyway while just putting an `extern "C"` in front of some C snippet code. – user2485710 Jul 29 '13 at 12:20
  • You'd need to `reinterpret_cast` to a reference to get the same type-punning. – Mike Seymour Jul 29 '13 at 12:20
  • @R.MartinhoFernandes why you don't care ? – user2485710 Jul 29 '13 at 12:20
  • 4
    @user2485710 because 1) who the hell needs this; and 2) `memcpy` works perfectly fine for this, with no need to pick weasel interpretations of the standard to make it work. – R. Martinho Fernandes Jul 29 '13 at 12:22
  • @R.MartinhoFernandes So `reinterpret_cast` is "worse" (in the sense that it has less use cases when it results in well-formed code) than union type punning? Also, as to "who the hell needs this": what if e. g. manipulating the exact byte representation of an object is needed? Or it shouldn't be necessary? –  Jul 29 '13 at 12:22
  • @MikeSeymour well, who is telling you that T and U are not references ? :) I'm just focusing on the business logic inside this 2, not on the type or what to pass to what. – user2485710 Jul 29 '13 at 12:22
  • @H2CO3 I still can't see why there is a so neat choice against the union while this kind of cast can lead to truncation while the union doesn't, Isn't the truncation something that you should care about ? – user2485710 Jul 29 '13 at 12:26
  • @user2485710 You definitely should. –  Jul 29 '13 at 12:28
  • @user2485710: If they were reference types, they couldn't be members of a union. So presumably, you're asking about type-punning between object types, in which case the cast needs to be `reinterpret_cast` to give the same punning as a dodgy union access. – Mike Seymour Jul 29 '13 at 12:28
  • @R.MartinhoFernandes how you can use `memcpy` on a variable of a type that is of a size that is not a multiple of 8bit/1byte ? – user2485710 Jul 29 '13 at 12:29
  • 3
    @R.MartinhoFernandes: To answer your question, "who the hell needs this," I do. – John Dibling Jul 29 '13 at 12:29
  • @user2485710: All types have a size which is a multiple of a byte. (The only partial exceptions are bitfields, but there are major restrictions on what you can do with them). – Mike Seymour Jul 29 '13 at 12:31
  • @MikeSeymour not really http://www.cplusplus.com/reference/cstdint/ – user2485710 Jul 29 '13 at 12:33
  • @user2485710 you will have to be more clear on what that link is supposed to mean. In C++ all types have sizes that are integral numbers of bytes, even the ones in that page. Just to be clear, all natural numbers are multiples of 1. – R. Martinho Fernandes Jul 29 '13 at 12:34
  • @R.MartinhoFernandes not really, assume that you declare `std::vector`, the size of every position in the vector is 1 bit, how memcpy works in this case ? my point is simple, not all the types are 1yte*k in terms of size. – user2485710 Jul 29 '13 at 12:37
  • @user2485710 http://coliru.stacked-crooked.com/view?id=fa1574cd78bfbd1a31c26086a35fdac2-566ba27bdbe9ec997145a2702f4e303b. 40 is a multiple of 1. http://coliru.stacked-crooked.com/view?id=35a3f3b2242328c053a0b409f916718b-566ba27bdbe9ec997145a2702f4e303b 16 is a multiple of 1 too. – R. Martinho Fernandes Jul 29 '13 at 12:38
  • @R.MartinhoFernandes http://stackoverflow.com/a/6782085/2485710 – user2485710 Jul 29 '13 at 12:41
  • @user2485710 I have no idea how that changes anything. Show me code that observes an object with a size that isn't a multiple of 1. – R. Martinho Fernandes Jul 29 '13 at 12:43
  • 1
    @user2485710: `std::vector` doesn't define a 1-bit type, since that's impossible: all types are a multiple of a byte. It packs bits into larger types. `memcpy` doesn't work in that case - there is no way to get a pointer to copy from. But this argument is getting very off-topic. – Mike Seymour Jul 29 '13 at 12:46
  • @R.MartinhoFernandes that's because you are using sizeof, which only returns and thinks in terms of bytes, in this case the standard is saying that this is a 1 bit wide position, You are "cheating" if you are using sizeof because you will never know if something is not a multiple of a byte ... – user2485710 Jul 29 '13 at 12:46
  • 3
    @user2485710: `sizeof` works in terms of bytes because all object sizes are a multiple of a byte. End of story. Now could you please stop this pointless argument. – Mike Seymour Jul 29 '13 at 12:48
  • @MikeSeymour you are the first saying that memcpy doesn't work in that case while memcpy clearly operates on k*byte wide types ... if you don't want to partecipate no one is forcing you ... – user2485710 Jul 29 '13 at 12:50
  • 1
    @user2485710 He means that `memcpy` cannot get that mystical 1-bit object, because you cannot either (seriously, try to write some code where your claim is exposed; spoiler warning: you cannot). `memcpy` doesn't work on `vector` for different reasons: it's not a trivially copyable type (and please, don't go around type punning `std::vector`; it ends in disaster) – R. Martinho Fernandes Jul 29 '13 at 12:51
  • @R.MartinhoFernandes well, that's a more well written explanation, and no, I'm not using `memcpy` that much because I try to avoid C style memory operations all the time. Since now I'm committed to find a type that is not k*byte wide ... – user2485710 Jul 29 '13 at 12:55
  • @user2485710: If you're thinking of bitfields, they are padded out. – John Dibling Jul 29 '13 at 12:59
  • @JohnDibling no I'm thinking about nothing in particular, it's just a challenge, there must be something like that in the wild ... – user2485710 Jul 29 '13 at 13:00
  • 1
    @user2485710: There is not. There are not many things in C++ that are impossible, but this is one. You cannot have a datatype whose size is not divisible by 1. – John Dibling Jul 29 '13 at 13:02

4 Answers4

8

Contrary to what the other answers state, from a practical point of view there is a huge difference, although there might not be such a difference in the standard.

From the standard point of view, reinterpret_cast is only guaranteed to work for roundtrip conversions and only if the alignment requirements of the intermediate pointer type are not stronger than those of the source type. You are not allowed (*) to read through one pointer and read from another pointer type.

At the same time, the standard requires similar behavior from unions, it is undefined behavior to read out of a union member other than the active one (the member that was last written to)(+).

Yet compilers often provide additional guarantees for the union case, and all compilers I know of (VS, g++, clang++, xlC_r, intel, Solaris CC) guarantee that you can read out of an union through an inactive member and that it will produce a value with exactly the same bits set as those that were written through the active member.

This is particularly important with high optimizations when reading from network:

double ntohdouble(const char *buffer) {          // [1]
   union {
      int64_t   i;
      double    f;
   } data;
   memcpy(&data.i, buffer, sizeof(int64_t));
   data.i = ntohll(data.i);
   return data.f;
}
double ntohdouble(const char *buffer) {          // [2]
   int64_t data;
   double  dbl;
   memcpy(&data, buffer, sizeof(int64_t));
   data = ntohll(data);
   dbl = *reinterpret_cast<double*>(&data);
   return dbl;
}

The implementation in [1] is sanctioned by all compilers I know (gcc, clang, VS, sun, ibm, hp), while the implementation in [2] is not and will fail horribly in some of them when aggressive optimizations are used. In particular, I have seen gcc reorder the instructions and read into the dbl variable before evaluating ntohl, thus producing the wrong results.


(*) With the exception that you are always allowed to read from a [signed|unsigned] char* regardless of that the real object (original pointer type) was.

(+) Again with some exceptions, if the active member shares a common prefix with another member, you can read through the compatible member that prefix.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • Interesting. The failure in the `reinterpret_cast`-implementation surprises me. Can this be considered a bug or is the standard simply too strict to use the cast for such applications? I am not familiar with networking in C++. – Marc Claesen Jul 29 '13 at 15:13
  • 2
    @MarcClaesen: Google for strict aliasing and some blunt word and you will find someone complaining on that behavior in GCC. The standard provides a set of situations for *valid aliasing* (multiple pointers referring to the same object), outside of the limited set, everything else is undefined behavior, and the case of the `reinterpret_cast` above is undefined behavior. For more info, google *'strict aliasing', 'no-strict-aliasing'* or something alike – David Rodríguez - dribeas Jul 29 '13 at 15:46
  • Excellent point regarding how the compilers handle this _practically_. In this case, the behaviour is implementation-defined (and really, how else _would_ a sane compiler define this, without wasting cycles to create random UB?) - and, to be honest, extremely useful in relevant cases. – underscore_d Feb 15 '16 at 10:44
  • I would like to see (a direct url to) an example where `reinterpret_cast` breaks. These two functions generate identical assembler output. – Maxim Egorushkin Mar 06 '17 at 10:58
  • @MaximEgorushkin: the second snippet comes out of a fix to our product, when compiled with `-O2`, without `-fno-strict-aliasing` and gcc (I believe it was 4.3 at the time) the optimiser reordered the instructions writing to the double before it read into the `int64_t` and run the `bswap` (for `htonll`). I have not checked newer versions. – David Rodríguez - dribeas Mar 08 '17 at 12:18
  • @DavidRodríguez-dribeas Okay then. I fail to see _huge difference_ and I am not aware of anything in the standard that makes union cast different from `reintrerpret_cast`, as you mentioned. _Compilers often provide additional guarantees for the union case_ - any supporting link for that? – Maxim Egorushkin Mar 08 '17 at 12:22
  • @MaximEgorushkin: Check the gcc manpage the description of `-fstrict-alias` (for example [here](https://linux.die.net/man/1/gcc)) – David Rodríguez - dribeas Mar 08 '17 at 14:28
  • @DavidRodríguez-dribeas It does not say that `reintrerpret_cast` breaks with `-fstrict-alias`. One way to interpret that note is: _one should really be using `reintrerpret_cast`, however, union cast still works._ – Maxim Egorushkin Mar 08 '17 at 15:21
5

There are some technical differences between a proper union and a (let's assume) a proper and safe reinterpret_cast. However, I can't think of any of these differences which cannot be overcome.

The real reason to prefer a union over reinterpret_cast in my opinion isn't a technical one. It's for documentation.

Supposing you are designing a bunch of classes to represent a wire protocol (which I guess is the most common reason to use type-punning in the first place), and that wire protocol consists of many messages, submessages and fields. If some of those fields are common, such as msg type, seq#, etc, using a union simplifies tying these elements together and helps to document exactly how the protocol appears on the wire.

Using reinterpret_cast does the same thing, obviously, but in order to really know what's going on you have to examine the code that advances from one packet to the next. Using a union you can just take a look at the header and get an idea what's going on.

John Dibling
  • 99,718
  • 31
  • 186
  • 324
  • @R.MartinhoFernandes: Until I can think of a good answer to your question, I'm going to remove that bit from my answer. – John Dibling Jul 29 '13 at 12:46
  • Fair enough. FWIW I believe you got it the wrong way around: alignment in a union is guaranteed right by the compiler, while `reinterpret_cast` to a type with a more strict alignment is dangerous. – R. Martinho Fernandes Jul 29 '13 at 12:47
  • @R.MartinhoFernandes: No, I know. – John Dibling Jul 29 '13 at 12:49
  • Since no "relevant" differences are popping out at this point, I'm going to accept this answer to prevent this question going really OT or on the metaphysics side . – user2485710 Jul 29 '13 at 12:52
  • @R.MartinhoFernandes: The standard only guarantees roundtrip conversion of pointers: `reinterpret_cast(reinterpret_cast(Tptr))` and only under the premise that the alignment constraints on `U` are *weaker* than those of `T`. It does not guarantee, for example: `reinterpret_cast(reinterpret_cast(charp))`, as some implementations might be unable to hold a `double*` referring to an unaligned location. – David Rodríguez - dribeas Jul 29 '13 at 12:54
  • @user2485710: It was a kind of tricky and narrow question to begin. For one thing, not many people do type-punning in real code I don't think, and for another all of this stuff is frowned upon anyway, at least academically. – John Dibling Jul 29 '13 at 12:55
  • @JohnDibling yep, type punning, what I'm really interested in, so I was trying to pick one while following a good rationale. – user2485710 Jul 29 '13 at 13:01
  • @user2485710: Besides the *documentation* purposes, and outside of what the standard guarantees, from a practical point of view compilers offer additional guarantees in the case of `union` – David Rodríguez - dribeas Jul 29 '13 at 13:25
1

In C++11, union is class type, you can an hold a member with non-trivial member functions. You can't simply cast from one member to another.

§ 9.5.3

[ Example: Consider the following union:

union U {
int i;
float f;
std::string s;
};

Since std::string (21.3) declares non-trivial versions of all of the special member functions, U will have an implicitly deleted default constructor, copy/move constructor, copy/move assignment operator, and destructor. To use U, some or all of these member functions must be user-provided. — end example ]

Jakub Arnold
  • 85,596
  • 89
  • 230
  • 327
billz
  • 44,644
  • 9
  • 83
  • 100
  • ok, what if I decide that T and U are just POD types ? In that case, considering your answer, you are basically saying that they are the same, right ? – user2485710 Jul 29 '13 at 12:25
  • You might add that unions are indeed classes that contain objects of different types at different times, which is something fundamentally different than reinterpreting types. – cli_hlt Jul 29 '13 at 12:25
  • @user2485710: It doesn't matter if they're POD types or not; as far as C++ is concerned, this is either implementation-defined behavior or *undefined* behavior. Unions only make sense to C++ if you access the variable you stored into them. C++ does not have a standard-protected mechanism for in-place type punning. – Nicol Bolas Jul 29 '13 at 12:29
-1

From a practical point of view, they're most probably 100% identical, at least on real, non-fictional computers. You take the binary representation of one type and stuff it into another type.

From a language lawyer point of view, using reinterpret_cast is well-defined for some occasions (e.g. pointer to integer conversions) and implementation-specific otherwise.

Union type punning, on the other hand is very clearly undefined behaviour, always (though undefined does not necessarily mean "doesn't work"). The standard says that the value of at most one of the non-static data members can be stored in a union at any time. This means that if you set var1 then var1 is valid, but var2 is not.
However, since var1 and var2 are stored at the same memory location, you can of course still read and write any of the types as you like, and assuming they have the same storage size, no bits are "lost".

Damon
  • 67,688
  • 20
  • 135
  • 185
  • It's not just about computers. It's about compilers as well. – R. Martinho Fernandes Jul 29 '13 at 12:37
  • Your final 2 paragraphs totally contradict each other. The last one is a practical consequence of efficient compiler implementation, not anything defined by the Standard. As reading a member other than the one most recently written to is UB, some theoretical - and awful - compiler would, sadly, be well within its rights to return totally random garbage, rather than the much saner (and practically done) alternative of simply reinterpreting the bit pattern. It's fine to say that you can do this _practically_, but it's not guaranteed on the language level, so that needs to be explicitly qualified – underscore_d Feb 15 '16 at 10:49
  • @underscore_d: The main reason why this is undefined is that (other than in C where it is well-defined) objects may have constructors/destructors that need to set up non-trivial state (that, and possible padding, but that's inconsequential for the example). If anything, the compiler is allowed to optimize out the entire scope (which is bullshit, but unluckily can be real), instead of returning whatever bits happen to be in the memory location. Returning garbage is a perverted interpretation of UB, this is in the same category as "allowed to format your harddisk". No compiler is ever... – Damon Feb 15 '16 at 11:01
  • ... allowed to intentionally trash some haphazard memory location just to "teach" you, no matter whether you have invoked UB or not. It's not required to do anything in particular as far as the language standard goes, but that doesn't mean it is allowed to be a complete dick. The binary contents of the memory cell may not change if you don't write to it (unless cosmic radiation flips a bit, or such). Insofar, the paragraphs do not contradict each other. You _can_ do it, it just isn't guaranteed that the compiler will do _anything_. – Damon Feb 15 '16 at 11:04
  • I wasn't saying it'd trash RAM, rather that - per my current understanding - it might (stupidly, but presumably) legally return gibberish that isn't even _from_ RAM or otherwise doesn't reflect the same bits, if **read**ing an 'inactive' union member. Yeah, unlikely, & I'd infinitely deride any compiler that was stupid enough to do this. Your example of the UB being optimised away altogether is more likely & useful (in a pedagogical sense: not for the hapless person who has to debug the result). Anyway, I avoid this sort of code as far as possible, & for the rest, GCC implements it intuitively – underscore_d Feb 15 '16 at 11:55
  • 1
    @underscore_d: You are perfectly right that a compiler could indeed choose to use an old (but valid!) numeric value of the struct member that it still has in a register. That's not just legitimate, but it even makes sense -- after all, since you didn't modify that member, the value by definition _is the same_ (logically, not factually). Luckily, this situation is very unlikely to happen, one would need to do several such conversions in a row, all within the same scope. – Damon Feb 15 '16 at 12:14
  • Yeah, & I'd guess that was the Standard's reasoning, as it follows the same (fair) logic as strict aliasing. However, it's interesting (& extremely relevant in several of my current projects...) that C++ makes an allowance for this if the `union` members share a common initial sequence of submembers. I guess the tedium of checking that for all `union`s is why practical compilers allow type-punning for all union members. (To confuse further, the C committee seem to have gotten confused & started thinking about aliasing instead, leading to this mess: http://stackoverflow.com/questions/34616086) – underscore_d Feb 15 '16 at 12:25