C type punning question
How do I make the below function generic for uint8_t, uint16_t, uint32_t, int8_t, int16_t, int32_t and float_t?
I don't like repeating the same logic in every case as you can see. The only difference in each case is the casting.
I'd ideally like a solution that adheres to the C standard and is hence portable. Any ideas are welcome.
Thanks.
static bool_t IsWithinLimits(const dbKey_t *key, const void *data)
{
bool_t isWithinLimits = TRUE;
limits_t limits = getDefinedLimits(key);
switch(key->type)
{
case TYPE_UINT8:
if((*(const UINT8*)data > (UINT8)limits.max) || (*(const UINT8*)data < (UINT8)limits.min))
{
isWithinLimits = FALSE;
}
break;
case TYPE_UINT16:
if((*(UINT16*)pData > (UINT16)limits.max) || (*(UINT16*)data < (UINT16)limits.min))
{
isWithinLimits = FA开发者_运维问答LSE;
}
break;
case TYPE_UINT32:
...
break;
case TYPE_INT8:
...
break;
case TYPE_INT16:
...
break;
case TYPE_INT32:
...
break;
case TYPE_FLOAT:
...
break;
}
return isWithinLimits;
}
There is no easy way to do generic programming like this in C. If you're worried about maintenance, then this may be a rare occasion where a macro is appropriate.
Why not use a macro?
#define DO_MY_WORK(TYPE)\
if((*(TYPE*)pData > (TYPE)tLimits.zMax) || (*(TYPE*)pData < (TYPE)tLimits.zMin))\
{\
isWithinLimits = FALSE;\
}
Well, you could extract the casts:
int64_t loadptr_uint8(const void *p) {
return *(uint8_t*)p;
}
int64_t convert_uint8(int64_t val) {
return (uint8_t)val;
}
int testLimits(const limits_t *plimits, const void *pData, int64_t(*loadptr)(void*), int64_t (*convert)(int64_t)) {
return loadptr(pData) <= convert(limits->max) && loadptr(pData) >= convert(limits->min);
}
switch(key->type) {
case TYPE_UINT8:
isWithinLimits = testLimits(&limits, pData, loadptr_uint8, convert_uint8);
break;
// etc
}
Or, if the various types form a contiguous range of values from 0, you could even make two arrays of function pointers and do:
bool isWithinLimits = testLimits(&limits, pData, loadptrs[key->type], converts[key->type]);
Notes:
- You still have to write two functions for each type, although they're easily macro-generated if you prefer.
- It doesn't really seem worth it for this little code.
- I've chosen
int64_t
since it is capable of representing all the values of all the integer types you use, so the conversions toint64_t
never discard information and never change the result of a comparison with respect to with doing the same comparison in the source type. But if you also wanted to coveruint64_t
, then you can't use the same type for everything, since there is no integer type that can represent all the values of all integer types. You'd also need a separatetestLimitsf
function forfloat
, perhaps usinglong double
as the common type for future flexibility. - [Edit: I just realised, assuming IEEE-754,
double
actually can exactly represent all the values of all the types you use. So with a slight portability restriction, you could usetestLimitsf
for everything and deal in doubles] - Are you sure that it's worth converting to (for example)
uint8_t
before comparison? Either the value is in range for auint8_t
, in which case you don't need to convert, you can just do the comparison. Or else the value isn't in range, in which case the modulo reduction makes the comparison a bit meaningless except in the special cases of 0 and -1. So it might be worth it, if something you haven't stated makes it so, but it looks fishy to me. - You said in a comment, "I am trying to make that function more efficient". This might go against that. It's logically possible to inline
testLimits
and also the calls to the casting functions in theswitch
, but I wouldn't count on it.
The next revision of the standard (C1x) will add support for type generic expressions (example from wikipedia):
#define cbrt(X) _Generic((X), long double: cbrtl, \
default: cbrt, \
float: cbrtf)(X)
gcc has some preliminary C1x support. I think _Generic is not yet supported, but keep it in mind for the future.
精彩评论