// After this next call, "t" holds the exception_ptr
// it's no longer in the union space.
auto t = std::move(rhs.spam);
No, it is in union space. rhs.spam is still an exception_ptr at this point. It is until the destructor is called. t is also one.
// There is no guarantee that the space ham is built in is
// empty but that doesn't matter, it will just overwrite
// anything currently there.
Writing over what is currently there is the opposite of "doesn't matter". It's undefined behavior. That always matters.
But.. exception_ptr is a shared_ptr, so if he moves it first, there is nothing that will happen by calling the destructor, it's a null-op.
You can't know that, period.