⚠ Warning: that code hasn’t been tested in production and should be considered experimental
I wonder if that would work, if you’re caring only about required(1) built-in types for which signedness is defined or can be determined from the preprocessor(2), or about any typedef of any of those types:// Copyright © 2019 mpan; <https://mpan.pl/>; CC0 1.0 (THIS CODE!)
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <wchar.h>
#define OVERLOAD_BY_SIGN(value, signedFn, unsignedFn) _Generic((value),\
unsigned char: (unsignedFn),\
unsigned short: (unsignedFn),\
unsigned int: (unsignedFn),\
unsigned long: (unsignedFn),\
unsigned long long: (unsignedFn),\
signed char: (signedFn),\
short: (signedFn),\
int: (signedFn),\
long: (signedFn),\
long long: (signedFn),\
char: OVERLOAD_BY_SIGN_CHAR((unsignedFn), (signedFn)),\
default: _Generic((value),\
_Bool: (unsignedFn), default: _Generic((value),\
size_t: (unsignedFn), default: _Generic((value),\
ptrdiff_t: (signedFn), default: _Generic((value),\
uintmax_t: (unsignedFn), default: _Generic((value),\
intmax_t: (signedFn), default: _Generic((value),\
uint_least16_t: (unsignedFn), default: _Generic((value),\
uint_least32_t: (unsignedFn), default: _Generic((value),\
uint_least64_t: (unsignedFn), default: _Generic((value),\
int_least16_t: (signedFn), default: _Generic((value),\
int_least32_t: (signedFn), default: _Generic((value),\
int_least64_t: (signedFn), default: _Generic((value),\
uint_fast8_t: (unsignedFn), default: _Generic((value),\
uint_fast16_t: (unsignedFn), default: _Generic((value),\
uint_fast32_t: (unsignedFn), default: _Generic((value),\
uint_fast64_t: (unsignedFn), default: _Generic((value),\
int_fast8_t: (signedFn), default: _Generic((value),\
int_fast16_t: (signedFn), default: _Generic((value),\
int_fast32_t: (signedFn), default: _Generic((value),\
int_fast64_t: (signedFn), default: _Generic((value),\
wchar_t: OVERLOAD_BY_SIGN_WCHAR((unsignedFn), (signedFn)),\
default: _Generic((value),\
default: (abort)\
)))))))))))))))))))))\
)(value);
#if CHAR_MIN < 0
#define OVERLOAD_BY_SIGN_CHAR(unignedFn, signedFn) (signedFn)
#else
#define OVERLOAD_BY_SIGN_CHAR(unignedFn, signedFn) (unsignedFn)
#endif
#if WCHAR_MIN < 0
#define OVERLOAD_BY_SIGN_WCHAR(unignedFn, signedFn) (signedFn)
#else
#define OVERLOAD_BY_SIGN_WCHAR(unignedFn, signedFn) (unsignedFn)
#endif
Use:#include <stdio.h>
static inline void signedCall(int const x) {
printf("signed: %d\n", x);
}
static inline void unsignedCall(unsigned int const x) {
printf("unsigned: %u\n", x);
}
int main(void) {
Type x = 1;
OVERLOAD_BY_SIGN(x, signedCall, unsignedCall);
return 0;
}
That definition can be extended as you wish by adding yet another line to the list: <Type>: (<unsignedFn|signedFn>), default: _Generic((value),\
A matching parenthesis must be added to the parens in the penultimate line. Unlike with plain _Generic, types can be duplicated in this structure.
The way this code works is as follows. It has two parts. The outer _Generic selector, which is really just an optimization for types that surely aren’t compatible with each other, and the inner _Generic·s sequence that performs the job for all other types. The inner _Generic·s are really a form of a linked list:,-------. ,-------. ,-------. ,-------. ,-------.
| Type1 | ,->| Type2 | ,->| Type3 | ,->| Type4 | ,->| Type5 |
| fn | | | fn | | | fn | | | fn | | | fn |
| o-----' | o-----' | o-----' | o-----' | o----> abort
`-------' `-------' `-------' `-------' `-------'
If the type of value doesn’t match the given node, it uses the default association(3), effectively going deeper into the list. The final node is using abort simply because there must be the default association there.(4) That requirement arises from the fact, that even if any “earlier” _Generic selector matches, the “later” ones still need to be valid: if the final one would not have the default association, it would be valid only for a single type, which would break the whole code.
The outer _Generic is just a plain old selector, which uses the inner one iff the type isn’t found on the list already.
Some platforms may lack things like wchar_t, so this is just a template to be adjusted as needed. It is also not completely eliminating supplying informtion from external sources during build, as described in the posts of other people here, but merely limits the amount of work to be done and streamlines access to it.
This code needs a compiler that eliminates unused static inline functions. If that requirement is not met, it will waste space in the final executable. If the compiler can do that for static functions it is even better.
Alternatively, if the final “(value)” portion of the macro definition is removed, the signedFn and unsignedFn arguments can actually be arbitrary expressions — as long as they can be used as arguments to a macro (consider putting them in parens if they contain the comma operator). The benefit is that the unused code is surely eliminated on any sane comiler and less code has to be written. The drawback is that if no type matches, the sentinel abort is a no-effect code that on some platforms may even be unreported. Consider replacing it with a call that will stop the code.
____
(1) Some compiler-specific built-in types have to be added manually, perhaps using conditional inclusion.
(2) E.g. time_t signedness can’t be determined.
(3) 6.5.1.1§2
(4) 6.5.1.1§2–3