For 32-bit ints, 64-bit int64s, and IEEE 64-bit doubles, the following trick works (apart from violating aliasing rules and whatnot):
double convert(int x) {
  double tricky = 0x1.8p53;
  int64 hack = (int64 &)tricky + x;
  return (double &)hack - 0x1.8p53;
}
Here I take tricky = 2^53 + 2^52.  The smallest representable change in this value is 1, meaning the significand is measured in units of 1.  The significand is stored in the low-order 52 bits of a double.  I won't overflow or underflow the significand by adding x to it (since x is 32-bit), so hack is the binary representation of 2^53 + 2^52 + x as a double.  Subtracting off 2^53 + 2^52 gives me x, but as a double.
(What follows, I think, is sorta close to x86-64 assembly code.  I don't see why it wouldn't do the right thing, but I haven't tested it.  Or even assembled it.)
movsx rax, dword ptr [x]
add rax, [tricky]
mov [hack], rax
fld [hack]
fsub st(0), [tricky]
fstp [answer]