Introduction
C++ has std::swap()
, but there is no standard equivalent in C. If you’re using C23, implementing SWAP()
is trivial:
#define SWAP(A,B) \
do { auto _tmp = (A); (A) = (B); (B) = tmp; } while (0)
For more about Preprocessor macros, and why the
do
...while
is a good idea in particular, see here.
That was easy. So we’re done, right? But what if you’re not using C23 and therefore can’t use auto
?
A Pre-C23 SWAP()
Since C prior to C23 doesn’t have either auto
(or typeof
), there’s no way to declare a variable that’s the type of some other variable or expression.
Yes, both
gcc
andclang
support__auto_type
andtypeof
as extensions and you can use them if you want; but I prefer to use either standard C or at least have a way to fall back to standard C if certain features aren’t available.
Instead, we can just declare a char
array that’s the right size and use it for temporary storage. Of course then we can’t simply use =
to do assignments, but must instead use memcpy()
:
#define SWAP(A,B) do { \
char _tmp[ sizeof((A)) ]; \
memcpy( _tmp, &(A), sizeof((A)) ); \
(A) = (B); \
memcpy( &(B), _tmp, sizeof((B)) ); } while (0)
You might think calling memcpy()
is less efficient than an assignment. The thing is that memcpy()
is kind of special in that the compiler does special optimization for it — enough such that the generated code is as efficient.
Alignment
The problem with the above macro is that _tmp
isn’t guaranteed to be properly aligned for an object of type A
. Normally, you could use _Alignas
, but that won’t work here because you’d need to say _Alignas(typeof(A))
and we can’t use typeof
.
Even though _Alignas
can alternatively take an expression, you can’t use _Alignas(A)
either because that does not mean “align to the type of the expression A
.” Instead it means “align to the value of the integral constant expression A
” — and since A
isn’t a constant expression, it won’t work.
Prior to C11 and the addition of _Alignas
, the common trick to create an aligned char
array was to enclose it within a union
:
#define SWAP(A,B) do { \
union { char buf[ sizeof((A)) ]; max_align_t align; } _tmp; \
memcpy( _tmp.buf, &(A), sizeof((A)) ); \
(A) = (B); \
memcpy( &(B), _tmp.buf, sizeof((B)) ); } while (0)
Including a member of type max_align_t
will force the union
(and the char
array within it) to be properly aligned for any type.
Safeguards
We can improve SWAP()
a bit by at least ensuring that A
and B
have the same size:
#define SWAP(A,B) do { \
static_assert( sizeof((A)) == sizeof((B)), \
"SWAP() arguments must have same size" ); \
union { char buf[ sizeof((A)) ]; max_align_t align; } _tmp; \
memcpy( _tmp.buf, &(A), sizeof((A)) ); \
(A) = (B); \
memcpy( &(B), _tmp.buf, sizeof((B)) ); } while (0)
Ideally, you want to ensure that A
and B
have the same type, not just size, but there’s unfortunately no way to do that without typeof
.
Safeguard for C23’s SWAP()
If you are using C23, you can improve SWAP()
by ensuring A
and B
have the same type (since typeof
and typeof_unqual
are available):
#define SWAP(A,B) do { \
static_assert( IS_SAME( typeof(A), typeof(B) ), \
"SWAP() arguments must have same type" ); \
auto _tmp = (A); (A) = (B); (B) = _tmp; } while (0)
where IS_SAME()
uses _Generic
and is:
#define IS_SAME(T,U) \
_Generic( *(T*)0, \
typeof_unqual(U): 1, \
default : 0 \
)
The *(T*)0
is needed to convert T
(a type) into an expression required by _Generic
. (Reminder: the expression isn’t evaluated so it doesn’t matter that it’s dereferencing a null pointer.)
The typeof_unqual(U)
is necessary to remove qualifiers, otherwise it would never match if U
had qualifiers. (Reminder: _Generic
discards qualifiers from the type of the controlling expression.)
Conclusion
Of course SWAP()
isn’t really necessary: you can always open-code a swap manually every time; but having it makes things a little nicer regardless.
Top comments (2)
Very interesting article!
I thought that because you can use
memcpy
to do type-punning (in a defined way), it would also handle alignment.But I'm still new to reading the C standard, so I'm still confused about pretty much everything, to be honest.
You don't need
memcpy()
to do type-punning in C. You can use aunion
all by itself; see here.In C++, you can not use
union
to do type-punning. Instead, you can usememcpy()
as one of 3 ways to do type-punning safely in C++; see here.memcpy()
copies bytes and knows nothing about either types or alignment. If you need alignment, the buffer you are copying into must already have been aligned as I did above.