Cast type with range limit
Asked Answered
F

2

5

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

Fitful answered 1/4, 2022 at 9:49 Comment(4)
What you seem to want is to clamp the value, not do a straight conversion (which will essentially lead to truncation of the upper bits). This is not supported in standard C, and you have no other way to solve it than through a function similar to the one you show.Odelet
In C the only choice are macros. Your case is is a bit painful to do with macros. Biggest problem is different case of constant and associated types.Desiderata
From the previous comments (and C++ std::clamp): #define CLAMP(v,lo,hi) (v)<(lo)?(lo):(v)>(hi)?(hi):(v). From here, you can define specific macros for each destination type you target.Folkway
clamping with the type's limits like this is also called saturation: is there a function in C or C++ to do "saturation" on an integer, Convert from int to char in C with cutoff, Clamping short to unsigned charBrana
B
4

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)
Brana answered 1/4, 2022 at 16:18 Comment(1)
cool I didn't know that C11 gained such functionality.Desiderata
D
2

IMO best way to do it is first map constant describing limits into constant with desiried case. Then define macro which will use this new constant.

So basic idea looks like this:

const int8_t min_of_int8 = INT8_MIN;
const int8_t max_of_int8 = INT8_MAX;
const uint8_t min_of_uint8 = 0;
const uint8_t max_of_uint8 = UINT8_MAX;
....

#define DEFINE_CONVERTER(SRC, DST) \
inline static DST ## _t convert_ ## SRC ## _to_ ## DST (SRC ## _t src) \
{ \
    return src < min_of_ ## DST ? min_of_ ## DST : (src > max_of_ ## DST ? max_of_ ## DST : (DST ## _t)src); \
}

DEFINE_CONVERTER(int32, uint8)
DEFINE_CONVERTER(int32, int8)
....

Here is test written using C++.

Test it carefully since some implicit conversion may lurking and breaking this macro for specific pair of types.

If you wish to have different pattern for function names (like this I8 U32) then do same trick as for constant and define respective typedef which name will contain desired short versions of types.

Note similar approach is used on OpenSSL to provide same functions for different types.

Desiderata answered 1/4, 2022 at 10:19 Comment(1)
Thanks Marek . Learnt a new unit testing framework catch2Gomorrah

© 2022 - 2024 — McMap. All rights reserved.