有范围限制的铸造类型

Lei*_*ert 5 c casting data-conversion clamp saturation-arithmetic

是否有一种优雅的方法可以将较大的数据类型转换为较小的数据类型而不导致结果溢出?

例如,转换260uint8_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;
}
Run Code Online (Sandbox Code Playgroud)

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

解决方案应该是 C 语言(可选 GCC 编译器扩展)。

phu*_*clv 4

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

#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)))
Run Code Online (Sandbox Code Playgroud)

您可以删除不必要的类型以使其更短。之后就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));
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,您可以限制几乎任何类型,包括浮点类型或者_Bool向支持列表添加更多类型。使用时注意字体宽度和符号问题

Godlbolt 上的演示

您还可以使用 GNUtypeof__auto_type扩展来使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))
Run Code Online (Sandbox Code Playgroud)

long long除了它在没有进行某些更改时不起作用的事实之外unsigned long long,这还意味着使用 2 的补码。您可以CLAMP使用负位宽度进行调用以指示有符号类型或调用CLAMP_SIGN/CLAMP_UNSIGN方向

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

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)
Run Code Online (Sandbox Code Playgroud)