That was the first alternative that I thought of. Experimenting with this:
int sign_extend(int val_11b) {
struct { int v : 11; } t = { val_11b };
return t.v;
}
in Compiler Explorer produces pretty much the same x86-64 assembly as the first function in the post (the shift left, then shift right version) under GCC, Clang, and MSVC when optimizations are turned on.
https://godbolt.org/z/d3Kf9fsE6
(But I do love that xor variant; that's really clever and clean.)