You cannot assign an integral type to a struct without telling C++ what that means. So, how do you do that?
If you properly define your constructor:
struct ab {
std::uint32_t a; std::uint32_t b;
constexpr ab(std::uint64_t i = 0) noexcept
: a(static_cast<std::uint32_t>(i))
, b(static_cast<std::uint32_t>(i>>32)) {
}
} __attribute__((packed));
This very clearly defines which part of the 64-bit int go where and how. And because it is inline defined the compiler will not actually emit a function but will always inline this.
You don't have to resort to dodgy UB-ridden constructs to get performant code:
ab test(std::uint64_t x) {
ab obj = x; // <------------------------ that's all!
return obj;
}
ab test_ub(std::uint64_t x) {
ab obj;
reinterpret_cast<std::uint64_t&>(obj) = x; // BAD! don't do this!
return obj;
}
Both these functions produce the same assembly:
test(unsigned long):
mov rax, rdi
ret
The compiler realises what you are doing in that constructor when initialising a packed struct and if the underlying architecture allows it, will emit a single mov
instruction.
Compilers are better at this than we are, let them do their job.