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).
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
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)
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)