Search code examples
cvoid-pointerstype-punning

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 = FALSE;
        }
        break;

      case TYPE_UINT32:
       ...
       break;

      case TYPE_INT8:
       ...
       break;

      case TYPE_INT16:
       ...
       break;

      case TYPE_INT32:
       ...
       break;

      case TYPE_FLOAT:
       ...
       break;
    }

  return isWithinLimits;
}

Solution

  • 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 to int64_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 cover uint64_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 separate testLimitsf function for float, perhaps using long 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 use testLimitsf 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 a uint8_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 the switch, but I wouldn't count on it.