具有范围限制的铸造类型

Cast type with range limit

有没有一种优雅的方法可以在不导致结果溢出的情况下将较大的数据类型转换为较小的数据类型?

例如将 260 转换为 uint8_t 应该导致 255 而不是 4.

一个可能的解决方案是:

#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;
}

虽然这个解决方案有效,但我想知道是否有更好的方法(无需创建大量转换函数)。

解决方案应使用 C(可选 GCC 编译器扩展)。

IMO 最好的方法是首先将描述限制的常量映射到具有所需情况的常量。然后定义将使用这个新常量的宏。

基本思路是这样的:

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

这里是test written using C++.

仔细测试它,因为一些隐式转换可能潜伏并破坏特定类型对的宏。

如果你希望函数名称有不同的模式(像这样 I8 U32)那么做与常量相同的技巧并定义相应的 typedef 哪个名称将包含所需的短类型的版本。

注意 OpenSSL 使用类似的方法为不同的类型提供相同的功能。

从 C11 开始,您可以使用新的 _Generic selection 功能

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

您可以删除不需要的类型以使其更短。之后就这样称呼它为 CLAMP(type, value)

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));
}

通过这种方式,您几乎可以限制任何类型,包括 floating-point 类型或 _Bool(如果您将更多类型添加到支持列表)。使用时注意字体宽度和符号问题

Demo on Godlbolt

您还可以使用 GNU typeof or __auto_type extensions 使 CLAMP 宏更干净、更安全。这些扩展也适用于较旧的 C 版本,因此您可以在无法访问 C11

的情况下使用它们

在旧 C 版本中执行此操作的另一种简单方法是指定目标位宽

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

除了它不适用于 long longunsigned long long 没有一些变化之外,这也意味着使用 2 的补码。可以用负位宽调用CLAMP表示有符号类型或者调用CLAMP_SIGN/CLAMP_UNSIGN方向

另一个缺点是它只是限定值而不会转换为预期的类型(但您可以使用 typeof__auto_type 如上所述 return 正确的类型)

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)