Search code examples
ccastingdata-conversionclampsaturation-arithmetic

Cast type with range limit


Is there an elegant way to cast a bigger datatype to a smaller one without causing the result to overflow?

E.g. casting 260 to uint8_t should result in 255 instead of 4.

A possible solution would be:

#include <limits.h>
#include <stdint.h>

inline static uint8_t convert_I32ToU8(int32_t i32)
{
  if(i32 < 0) return 0;
  if(i32 > UINT8_MAX) return UINT8_MAX;
  return (uint8_t)i32;
}

Although this solution works, I wonder if there is a better way (without having to create lots of conversion functions).

Solution should be in C (with optionally GCC compiler extensions).


Solution

  • Since C11 you can use the new _Generic selection feature

    #define GET_MIN(VALUE) _Generic((VALUE), \
        char        : CHAR_MIN,              \
        signed char : SCHAR_MIN,             \
        short       : SHRT_MIN,              \
        int         : INT_MIN,               \
        long        : LONG_MIN,              \
        long long   : LLONG_MIN,             \
        default     : 0 /* unsigned types */)
    
    #define GET_MAX(VALUE) _Generic((VALUE), \
        char                : CHAR_MAX,      \
        unsigned char       : UCHAR_MAX,     \
        signed char         : SCHAR_MAX,     \
        short               : SHRT_MAX,      \
        unsigned short      : USHRT_MAX,     \
        int                 : INT_MAX,       \
        unsigned int        : UINT_MAX,      \
        long                : LONG_MAX,      \
        unsigned long       : ULONG_MAX,     \
        long long           : LLONG_MAX,     \
        unsigned long long  : ULLONG_MAX)
    
    #define CLAMP(TO, X) ((X) < GET_MIN((TO)(X))    \
        ? GET_MIN((TO)(X))                          \
        : ((X) > GET_MAX((TO)(X)) ? GET_MAX((TO)(X)) : (TO)(X)))
    

    You can remove the unnecessary types to make it shorter. After that just call it as CLAMP(type, value) like this

    int main(void)
    {
        printf("%d\n", CLAMP(char, 1234));
        printf("%d\n", CLAMP(char, -1234));
        printf("%d\n", CLAMP(int8_t, 12));
        printf("%d\n", CLAMP(int8_t, -34));
    
        printf("%d\n", CLAMP(unsigned char, 1234));
        printf("%d\n", CLAMP(unsigned char, -1234));
        printf("%d\n", CLAMP(uint8_t, 12));
        printf("%d\n", CLAMP(uint8_t, -34));
    }
    

    This way you can clamp to almost any types, including floating-point types or _Bool if you add more types to the support list. Beware of the type width and signness issues when using it

    Demo on Godlbolt

    You can also use the GNU typeof or __auto_type extensions to make the CLAMP macro cleaner and safer. These extensions also work in older C versions so you can use them in you don't have access to C11


    Another simple way to do this in older C versions is to specify the destination bitwidth

    // Note: Won't work for (unsigned) long long and needs some additional changes
    #define CLAMP_SIGN(DST_BITWIDTH, X)                  \
        ((X) < -(1LL << ((DST_BITWIDTH) - 1))            \
        ? -(1LL << ((DST_BITWIDTH) - 1))                 \
        : ((X) > ((1LL << ((DST_BITWIDTH) - 1)) - 1)     \
            ? ((1LL << ((DST_BITWIDTH) - 1)) - 1)        \
            : (X)))
    
    #define CLAMP_UNSIGN(DST_BITWIDTH, X)                \
        ((X) < 0 ? 0 :                                   \
            ((X) > ((1LL << (DST_BITWIDTH)) - 1) ?       \
                ((1LL << (DST_BITWIDTH)) - 1) : (X)))
    
    // DST_BITWIDTH < 0 for signed types, > 0 for unsigned types
    #define CLAMP(DST_BITWIDTH, X) (DST_BITWIDTH) < 0    \
        ? CLAMP_SIGN(-(DST_BITWIDTH), (X))               \
        : CLAMP_UNSIGN((DST_BITWIDTH), (X))
    

    Beside the fact that it doesn't work for long long and unsigned long long without some changes, this also implies the use of 2's complements. You can call CLAMP with a negative bit width to indicate a signed type or call CLAMP_SIGN/CLAMP_UNSIGN direction

    Another disadvantage is that it just clamps the values and doesn't cast to the expected type (but you can use typeof or __auto_type as above to return the correct type)

    Demo on Godbolt

    CLAMP_SIGN(8, 300)
    CLAMP_SIGN(8, -300)
    CLAMP_UNSIGN(8, 1234)
    CLAMP_UNSIGN(8, -1234)
    CLAMP(-8, 1234)
    CLAMP(-8, -1234)
    CLAMP(8, 12)
    CLAMP(8, -34)