DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on • Edited on

A Generic SWAP() Macro in C

#c

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)
Enter fullscreen mode Exit fullscreen mode

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 and clang support __auto_type and typeof 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

where IS_SAME() uses _Generic and is:

#define IS_SAME(T,U)      \
  _Generic( *(T*)0,       \
    typeof_unqual(U): 1,  \
    default         : 0   \
  )
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
gberthiaume profile image
G. Berthiaume • Edited

Very interesting article!
I thought that because you can use memcpy to do type-punning (in a defined way), it would also handle alignment.

If a value is copied into an object having no declared type using memcpy or memmove, or is copied as an array of character type, then the effective type of the modified object for that access and for subsequent accesses that do not modify the value is the effective type of the object from which the value is copied, if it has one.
port70.net/~nsz/c/c11/n1570.html#6...

But I'm still new to reading the C standard, so I'm still confused about pretty much everything, to be honest.

Collapse
 
pauljlucas profile image
Paul J. Lucas

You don't need memcpy() to do type-punning in C. You can use a union all by itself; see here.

In C++, you can not use union to do type-punning. Instead, you can use memcpy() 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.