检查未知符号的变量是否在区间内

9
在一些C99代码中,我需要检查变量i是否在区间[0, max]内,其中max被认为是正数。问题是该变量的类型可以是有符号的和无符号的(通过更改typedef)。如何最好地检查变量是否在区间内?
直接的方法是:
bool is_in_interval(my_type i, my_type max) {
    assert(max > 0);
    return (i >= 0 && i <= max);
}

这将在typedef int my_type;的情况下正常工作。但是当my_type为无符号数时(即typedef unsigned int my_type;),i >= 0始终为真,编译器会(正确地)警告此问题,我想避免这种情况。(我不想关闭该警告,因为当实际上出现这样的比较时,它是有用的,而仅仅忽略编译器警告并不是一个好主意。)
我的当前想法是将i转换为无符号类型,只检查上界:
bool is_in_interval(my_type i, my_type max) {
    assert(max > 0);
    return ((size_t) i <= (size_t) max);
}

如果有符号类型具有二进制补码表示,则将任何负值强制转换为无符号版本后应大于max,是吗?如果是这样,那么这应该可以工作。然而,我不确定这是否是明智的方法。例如,假设所有平台上的有符号类型都使用二进制补码是安全的吗?有更好的方法吗?

当您的已签名测试值被解释为“无符号”时,如果它大于(unsigned)MAX/2,则此方法将无效。这种情况可能发生吗?(嗯,也许我是指小于。) - Jongware
2
我非常确定,将一个负值强制转换为兼容的无符号类型后,它将大于原始类型的任何正值。然而,如果你这样做,你会听到人们抱怨“奇怪的代码”。 - sh1
@Jongware 抱歉,我不太明白你的意思。MAX 不是类型的限制(例如 INT_MAXUINT_MAX)。使用大写字母会产生误导(现在已更正)。 - Fredrik Savje
1
@JohnColeman 我没有任何宗教上的偏见来忽略编译器警告。然而,我认为不应该有任何警告。否则,就必须跟踪文件和行号,确定哪些警告是可以接受的,哪些是不可以接受的。在这种情况下,错过真正的问题的风险似乎相当高。 - Fredrik Savje
1
一个 C 语言实现可以有比 size_t 更宽的整数类型(非正式地说,更大),如果是这样,那么强制转换可能会改变值并给出错误结果。来自 <stdint.h>uintmax_t 可以在 C99 及更高版本中避免此问题(但您说过是 C99)。另一方面,2sC 不是问题;在 C 中将有符号整数转换为无符号整数被定义为有效地加上或减去 2,即使在(非常罕见的)1sC 或 S&M 机器上也是如此。 - dave_thompson_085
显示剩余11条评论
5个回答

8

请检查是否为0和max。但避免直接进行>= 0测试。

强制类型转换是一个问题,因为它可能以某种实现定义的方式缩小值。

bool is_in_interval(my_type i, my_type max) {
  // first, take care of the easy case
  if (i > max) return false;
  if (i > 0) return true;
  return i == 0;
}

我也会再思考一下这个问题。


谢谢!您能详细说明一下“强制转换可能会以某种实现定义的方式缩小值”的问题吗?我不太明白您的意思。 - Fredrik Savje
1
@Fredrik Savje 将 long long(也许是 my_type 的类型)转换为 unsigned 是被定义良好的,但很可能会失去一些范围,因此 ((size_t) i <= (size_t) max) 就没有意义了,因为强制转换可能已经改变了 max 的值,但没有改变 i 的值。 - chux - Reinstate Monica
1
@sh1 这种方法有两个问题:强制转换后的负数 i 可能超过强制转换后的 max - 要想变成 >, 需要假设某种整数布局,虽然这种布局很常见,但 C 语言并没有规定。系统以一种含糊其辞的方式支持超过 uintmax_t 的整数类型(例如,不称之为“整数”的 128 位类型)。尽管我认为你的想法在99.999%的情况下都是可行的。 - chux - Reinstate Monica
1
@Fredrik Savje 我看到了4种方法:预处理器、在代码中隐藏>=(如上所示)、_Generic()、第二级函数is_in_interval2(i, 0, max)。预处理器无法理解my_type,因为它只使用intmax_t。用一个连续点来隐藏大于等于。(严谨的,volatile my_type j = i; if (j > 0) return true; return j == 0;_Generic()很好用,但是以可移植的方式枚举所有有符号/无符号类型比较困难。 - chux - Reinstate Monica
1
@chux,这个问题不仅存在于非标准的int类型,也存在于floatdouble类型中。我认为,要使UINTMAX_MAX小于两倍的INTMAX_MAX,需要让uintmax_t有一个垃圾位。我承认这对于任何类型都是可能的,但最有可能出现在16位类型(DSP)中。 - sh1
显示剩余5条评论

3
我认为这应该可以让编译器闭嘴了:
bool is_in_interval(my_type i, my_type max) {
  my_type zero = 0;
  assert(max > 0);
  return (zero <= i && i <= max);
}

它可以让一些编译器安静下来,但不是所有的。编译器在测试声明为“const”变量时是否会触发警告,如“条件始终为真”或“条件始终为假”方面存在差异。 - Peter
@Peter 我明白了,谢谢!那么它就变得不那么吸引人了。 - Fredrik Savje
也许是 volatile signed char zero = 0; - chux - Reinstate Monica
@chux,我以前在这种情况下使用过volatile,但是特别是为了说服调试器取消优化代码,以便我可以在调试器中编辑值。因此,我知道在至少一些编译器中,它会导致性能损失。 - sh1
嗯,我从未考虑过类型可能是易变的这种可能性。通常当我尝试在类型上放置此类属性时,它们是通过未正确实现的扩展完成的,我发现它们会被悄悄地丢弃,然后我只会得到有缺陷的代码。 - sh1
显示剩余2条评论

2
也许这是另一种直接的方法:
bool is_in_interval(my_type i, my_type max) {
    assert(max > 0);
    if ((my_type)-1 > 0)
        return (i <= max); // my_type is unsigned
    else
        return (0 <= i && i <= max); // my_type is signed
}

“if ((my_type)-1 > 0)”会产生警告,因为它总是为真或假。就像楼上所说的“总是为真,编译器会(正确地)发出警告”一样。 - chux - Reinstate Monica
好的,clang确实给了我一个警告,但是它是在return (0 <= i && i <= max); // my_type is signed上,说comparison of 0 <= unsigned expression is always true - nalzok

2
如果您的编译器支持,我认为最干净的解决方案是保持代码不变,但使用#pragma指令在本地禁用错误警告。例如,对于GCC 4.6.4+,以下内容将会做到这一点:如果您的编译器支持,我认为最干净的解决方案是保持代码不变,但使用#pragma指令在本地禁用错误警告。例如,对于GCC 4.6.4+,以下内容将会做到这一点:do the trick
bool is_in_interval(my_type i, my_type max) {
    assert(max > 0);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtype-limits"
    /* i >= 0 could warn if my_type is unsigned */
    return (i >= 0 && i <= max);
#pragma GCC diagnostic pop
}

显然,完全相同的#pragma 在Clang中也应该有效,因为它模仿了GCC的语法。其他编译器可能有类似的语法(例如#pragma warning(disable: ...)用于Visual C/C++),可以使用预处理指令来选择适当的#pragma用于每个编译器。
这种方法的主要缺点是美观性差——它会在代码中散布丑陋的#pragma。话虽如此,我认为这比故意混淆(并可能使代码失去优化)你的代码以避免编译器警告更可取。至少,对于下一个阅读代码的程序员来说,通过#pragma,清楚地知道为什么代码被编写成这样。
为了使代码更加干净和可移植,您可以使用 C99 _Pragma() 运算符 来定义禁用和重新启用警告的宏,例如像这样:
#if __GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__ >= 40604
#  define NO_TYPE_LIMIT_WARNINGS \
       _Pragma("GCC diagnostic push") \
       _Pragma("GCC diagnostic ignored \"-Wtype-limits\"")
#  define RESTORE_WARNINGS \
       _Pragma("GCC diagnostic pop")
/* Add checks for other compilers here! */
#else
#  define NO_TYPE_LIMIT_WARNINGS /* nothing */
#  define RESTORE_WARNINGS /* nothing */
#endif

"并像这样使用它们:"
bool is_in_interval(my_type i, my_type max) {
    assert(max > 0);
    NO_TYPE_LIMIT_WARNINGS; /* i >= 0 could warn if my_type is unsigned */
    return (i >= 0 && i <= max);
    RESTORE_WARNINGS;
}

或者,如果您想支持早期的C99编译器,可以创建一对头文件(不要添加任何包含保护!)命名为warnings_off.hwarnings_restore.h,其中包含每个编译器所需的适当#pragma,然后用以下代码括起来以消除警告:
#include "warnings_off.h"
/* ...code that emits spurious warnings here... */
#include "warnings_restore.h"

我喜欢这个想法,但是我看到了可移植性的一个很大的缺点:有很多非gcc C编译器(和程序员),包括可能是OP之一,需要重新编写/ #ifdef代码。 - chux - Reinstate Monica
谢谢!从某种意义上说,这可能是“正确”的方法。到目前为止,所有提出的解决方案基本上都是欺骗编译器不发出应该发出的警告的方法——显然的解决方案是暂时禁用警告。问题在于我正在编写一个小型C库,因此我不能依赖于特定的编译器。 - Fredrik Savje

2

一个简单的解决方案是进行双侧检验。

唯一真正损失的属性是适度的效率/性能。没有其他问题。

尽管 OP 肯定已经考虑过这个问题,但任何解决方案都应该以此作为参考权衡利弊。

bool is_in_interval2(my_type i, my_type min, my_type max) {
  return i >= min && i <= max;
}

// Could make this a macro or inline
bool is_in_interval(my_type i, my_type max) {
  return is_in_interval2(i, 0, max);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接