I have a slice of packed unions and I’d like to find the index of one particular value in the slice. Unfortunately, using std.mem.indexOfScalar doesn’t work because it tries to compare items using the equality operator ==. This works perfectly fine for packed structs, but not packed unions and I’m wondering why.
Interestingly, when I simply nest the packed union inside of a packed struct, then suddenly the compiler is fine with it (although I’m not 100% sure if it works as expected or if the results I’m seeing are just a coincidence).
As it stands, this program prints out false, true, false – seems to work as expected. But use Int directly and suddenly it doesn’t compile with the error operator == not allowed for type 'Int'.
Is there a good reason for this? Or did they just kind of forget to implement this for packed unions when they added it for packed structs? I couldn’t find a GitHub Issue on this, so I’d like to open one but I want to save myself the embarrassment in case it’s actually intentional
I haven’t read the relative proposal, but it seems to me that two equal bit patterns in two unions with differing active tags should not cause an equality check to pass unless the user explicitly “discards” the tag by extracting the value from the union. So for this reason it would make sense to me to not allow direct comparison.
Clearly this should in fact be false, yes. The tag is a bit pattern as well, so the two packed unions, therefore, are not bit-identical.
I believe this, rather than a compile error, is the result OP anticipated from using == to compare two packed unions. Comparing their values explicitly could be true, as is the case for the values of the payloads of any union variants which can be so compared. Or comparable fields of a packed struct, and so on.
packed union is not on the operator list for equality, but surely this is an oversight, or an NYI. It doesn’t make sense that wrapping a type in another type, with no other changes, would make something eligible for an equality check when it previously was not. I recall that being the logic by which packed structs became equality comparable in the first place.
Then again, the struct wrapper should be zero cost, so telling the compiler that you want it to work, by stuffing it into a spurious struct, is an ok workaround in the meantime.
Okay, so what y’all are saying makes perfect sense to me.
But suppose I really did want to compare two packed unions for bit-equality – how would I go about that?
Would I have to bit-cast them to an integer of the correct bit-size manually and then compare those? Does that even work well with large unions?
This is a good question. A packed union doesn’t have an active tag, so packed union { x: i32, y: u32 } should be as unambiguously equality-comparable as packed struct { x: i32 }.
I can only speculate on some reasons why equality comparisons between packed unions haven’t been implemented:
No one though of it, neither when proposing it nor when implementing it. Honestly, this is probably the most likely answer.
struct did not previously define any ==/!= comparisons, so support for comparing packed struct could be implemented without issue and have an unambiguous meaning. However, union does already define ==/!= for comparing the active union tag, which means that support for comparing packed union isn’t as straightforward (language design-/specs-wise and implementation-wise).
packed union currently lets you define unions containing fields of different sizes, e.g. packed union { x: u32, y: u16 }, which means that the handling of the padding bits needs to be taken into account. What will U{ .x = 1 } == U{ .y = 1 } yield? There is an accepted proposal for requiring all fields to have the same bit-size so maybe the question about equality comparisons will be revisited after that proposal is implemented.
Also, I only just now realized that packed unions don’t have backing integers, neither implicit nor explicit. There’s no backing_integer field in std.builtin.Type.Union and packed union(u64) is not valid syntax. A bit weird, maybe it will be revisited when the proposal linked above is implemented. Either way, the lack of a preexisting backed integer is another possible reason to add to the pile.
Bitcast them to an integer type of the same bitsize and compare them that way. The most portable way would be doing something like @as(@Type(.{ .int = .{ .signedness = .unsigned, .bits = @bitSizeOf(U) } }), @bitCast(u)). I believe that is more or less equivalent to how comparisons between packed structs are handled under the hood. Performance-wise it shouldn’t be an issue unless your unions are absurdly large.
Maybe unrelated to the topic but anyway a question…
I saw in std.meta.eql a field by field comparison which (I think) is recursively comparing everything.
Isn’t that terribly slow? Or is the compiler again outsmarting me?
Oh. That changes my opinion, I didn’t spot the lack of a tag in the packed union definition the first time around. It’s less obvious that packed should trump “it’s nonsense to compare bare unions unless they’re known to be the same variant”, in that case.
Although it makes it somewhat strange that you can toss that into a packed struct and now you can compare bit-for-bit. As you point out, something in the rest of the tagged struct could, maybe, distinguish the variants, I mean that would be good style at least.
I’m guessing the intended use here is to allow a packed struct to have the tag, while giving more control over which bytes are which than we would have available with a packed union(enum). That much makes sense, for a packed union(enum) you’d have only two choices, neither would be always-ideal, and that’s before considering that the struct could have more than one union in it.
It’s pretty low-stakes that you can’t compare a bare packed union with ==, since you can have an abstract packed struct type to do it with if that’s important, so while I find that choice strange† I do track the logic now.
† It becomes one of those things you just have to know about the language, and those should be conserved. There’s an exception to “if it can go in a packed struct, you can use ==”, where otherwise there would not be.
There are also some potential genuine uses where bit identity of packed union variants could be good / useful. Like a 16 value enum which is also a packed struct of four booleans: you can treat this as four orthogonal states, and you can switch on the full combination as an enum. I’ve used data structures like this, just takes a bit of casting, but using union variants would be more eloquent.
Which lead me to wonder if there are safety tags involved, and the answer is, no: