使用无符号整型比使用有符号整型更容易导致 bug 吗? 为什么?

谷歌 C + + 风格指南中,关于“无符号整数”的主题,建议

由于历史原因,C + + 标准还使用无符号整数来表示容器的大小——标准机构的许多成员认为这是一个错误,但在这一点上实际上是不可能修复的。事实上,无符号算法并不建模一个简单整数的行为,而是由标准来定义建模同余关系(包围在溢出/下溢上) ,这意味着编译器无法诊断出一类重要的错误。

同余关系有什么问题? 这难道不是无符号 int 的预期行为吗?

指南中提到了什么类型的 bug (一个重要的类) ? 溢出的 bug?

不要仅仅使用无符号类型来断言变量是非负的。

我之所以考虑在无符号整型上使用有符号整型,是因为如果它确实溢出(变为负值) ,就更容易检测到。

7777 次浏览

如前所述,混合 unsignedsigned可能导致意外的行为(即使定义明确)。

假设你想遍历除了最后五个元素之外的所有向量元素,你可能会写错:

for (int i = 0; i < v.size() - 5; ++i) { foo(v[i]); } // Incorrect
// for (int i = 0; i + 5 < v.size(); ++i) { foo(v[i]); } // Correct

假设 v.size() < 5,那么,因为 v.size()unsigned,所以 s.size() - 5将是一个非常大的数字,因此 i < v.size() - 5将是 truei的预期值范围更大。然后 UB 很快就发生了(一旦 i >= v.size()就失去了绑定访问)

如果 v.size()将返回有符号值,那么 s.size() - 5将为负,在上述情况下,条件将立即为 false。

另一方面,索引应该在 [0; v.size()[之间,所以 unsigned是有意义的。 Signed 也有它自己的问题,如 UB 带有溢出或实现定义的行为,用于负的有符号数的右移位,但迭代的 bug 来源较少。

最令人毛骨悚然的错误示例之一是当您使用 MIX 有符号和无符号值时:

#include <iostream>
int main()  {
auto qualifier = -1 < 1u ? "makes" : "does not make";
std::cout << "The world " << qualifier << " sense" << std::endl;
}

输出:

这个世界毫无意义

除非你有一个普通的应用程序,否则你不可避免地会遇到有符号和无符号值之间的危险混合(导致运行时错误) ,或者如果你发出警告并使它们成为编译时错误,你的代码中会有大量的 static _ cast。这就是为什么对于数学或逻辑比较类型,最好严格使用有符号整数。只对位掩码和表示位的类型使用无符号。

根据数值的预期域建模一个无符号的类型是一个糟糕的主意。大多数数字接近0而不是接近20亿,因此对于无符号类型,很多值都更接近有效范围的边缘。更糟糕的是,期末考试值可能在一个已知的正值范围内,但是在计算表达式时,中间值可能下溢,如果以中间形式使用,则可能是非常错误的值。最后,即使你的值被期望总是正的,这并不意味着它们不会与 其他变量交互,而 可以是负的,所以你最终会被迫混合有符号类型和无符号类型,这是最糟糕的情况。

为什么使用无符号整型比使用有符号整型更容易导致 bug 呢?

使用 没签名类型并不比对某些类别的任务使用 签了类型更容易导致 bug。

使用正确的工具完成工作。

同余关系有什么问题? 这难道不是无符号 int 的预期行为吗?
为什么使用无符号整型比使用有符号整型更容易导致 bug 呢?

如果任务匹配得好: 没有问题。不,不太可能。

安全性、加密和身份验证算法依赖于无符号模数学。

压缩/解压算法以及各种图形格式对 没签名数学也有好处,而且缺陷较少。

任何时候使用位操作符和移位,没签名操作都不会因为 签了数学的符号扩展问题而混乱。


符号整数数学有一个直观的外观和感觉容易理解的所有包括学习者的编码。C/C + + 最初不是目标,现在也不应该成为一种介绍语言。对于使用关于溢出的安全网的快速编码,其他语言更适合。对于精益快速代码,C 假设编码人员知道他们在做什么(他们有经验)。

如今,签了数学的一个缺陷是无处不在的32位 int,它有如此多的问题,在不进行范围检查的情况下,对于常规任务来说足够宽了。这会导致自满情绪,认为溢出不是针对的。相反,for (int i=0; i < n; i++) int len = strlen(s);被认为是可以的,因为 n被假定为 < INT_MAX,字符串永远不会太长,而不是在第一种情况下被全范围保护,或者在第二种情况下使用 size_tunsigned甚至 long long

C/C + + 是在一个包含16位和32位 int的时代发展起来的,而无符号的16位 size_t提供的额外位是非常重要的。无论是 int还是 unsigned都需要注意溢出问题。

谷歌在非16位 int/unsigned平台上的32位(或更广泛)应用,使得人们对 int的 +/-溢出缺乏关注,因为它的范围很广。这对于这种应用程序鼓励 int超过 unsigned是有意义的。然而,int数学并没有得到很好的保护。

狭窄的16位 int/unsigned关注点今天适用于选择的嵌入式应用程序。

谷歌的指导方针很好地适用于他们今天编写的代码。对于更大范围的 C/C + + 代码,这并不是一个明确的指导方针。


我之所以考虑在无符号整型上使用有符号整型,是因为如果它确实溢出(变为负值) ,就更容易检测到。

在 C/C + + 中,有符号整型数学溢出是 未定义行为,因此不一定比定义的 没签名数学行为更容易检测。


正如 @ 克里斯 · 乌达维尼斯所说,所有人(特别是初学者)最好避免混合使用 签了没签名,否则在需要时要仔细编码。

这里的一些答案提到了有符号和无符号值之间令人惊讶的提升规则,但是这看起来更像是一个与 混音有符号和无符号值有关的问题,并且不一定解释为什么在混合场景之外,签了变量优于 没签名变量。

根据我的经验,除了混合比较和提升规则之外,有两个主要原因导致无符号值成为 bug 磁铁,如下所示。

无符号值在零处有不连续性,这是编程中最常见的值

无符号整数和有符号整数在其最小值和最大值处都有一个 UINT_MAX2,它们在其中包围(无符号)或引起未定义行为(有符号)。对于 unsigned,这些点位于 UINT_MAX3和 UINT_MAX。对于 int,它们是在 INT_MININT_MAX。在具有4字节 int值的系统上,INT_MININT_MAX的典型值是 -2^312^31-1,在这样的系统上,UINT_MAX的典型值是 UINT_MAX1。

unsigned不适用于 int的主要错误诱导问题是它有一个 零不连续性。当然,零是程序中非常常见的值,还有其他一些小值,比如1、2、3。在不同的结构中,小值的加减是很常见的,尤其是1,如果你从 unsigned值中减去任何值,而它恰好是零,你就得到了一个大量的正值和一个几乎确定的 bug。

考虑除 last0.5之外的代码按索引迭代向量中的所有值:

for (size_t i = 0; i < v.size() - 1; i++) { // do something }

这个工作很好,直到有一天你传递一个空向量。不是零迭代,而是 v.size() - 1 == a giant number1,您将执行40亿次迭代,并且几乎有一个缓冲区溢出漏洞。

你需要这样写:

for (size_t i = 0; i + 1 < v.size(); i++) { // do something }

因此,在这种情况下,它可以被“修复”,但是只能通过仔细考虑 size_t的无符号特性。有时候你不能应用上面的修正,因为你有一些变量偏移,而不是一个常量,你想要应用,这可能是正面或负面的: 所以你需要把它放在比较的“一边”取决于有符号-现在的代码变得 真的混乱。

试图迭代到零并包含零的代码也存在类似的问题。像 while (index-- > 0)这样的代码可以正常工作,但是表面上等效的 while (--index >= 0)永远不会因为无符号值而终止。编译器可能会在右边为 字面意思零时发出警告,但如果它是在运行时确定的值,则肯定不会发出警告。

对位

有些人可能会说,有符号的值也有两个不连续性,那么为什么要选择无符号的值呢?不同之处在于,这两个不连续性都非常(最大程度上)远离零。我确实认为这是一个单独的“溢出”问题,有符号和无符号值都可能在非常大的值时溢出。在许多情况下,由于对值的可能范围的限制,溢出是不可能的,并且许多64位值的溢出在物理上可能是不可能的)。即使可能,与溢出相关的 bug 和 无符号值也会发生溢出相比,发生的几率通常也很小。因此,无符号结合了两个世界中最糟糕的情况: 可能会溢出非常大的量级值,以及零的不连续性。署名只有前者。

许多人会认为“你失去了一点”与未签名。这通常是正确的,但并不总是正确的(如果你需要表示无符号值之间的差异,你无论如何都会丢失这一点: 很多32位的东西都被限制在2GiB,或者你会有一个奇怪的灰色区域,在那里一个文件可以是4GiB,但是你不能使用某些 API 在第二个2GiB 的一半)。

即使在没有签名的情况下也能买到一些东西: 它买不到多少东西: 如果你必须支持超过20亿个“东西”,你可能很快就要支持超过40亿个东西。

逻辑上,无符号值是有符号值的子集

从数学上讲,无符号值(非负整数)是有符号整数的子集(就叫做 _ 整数)。2.然而,仅对 没签名值(如减法)执行操作时,签了值自然会弹出。我们可以说在减法下无符号值不是 关门了。有符号值不是这样。

要在文件中找到两个无符号索引之间的“ delta”吗?你最好按正确的顺序做减法,否则你会得到错误的答案。当然,您通常需要一个运行时检查来确定正确的顺序!当将无符号值作为数字处理时,您常常会发现(逻辑上)有符号的值总是出现,所以您不妨从有符号开始。

对位

如上面脚注(2)所述,C + + 中的有符号值实际上不是大小相同的无符号值的子集,因此无符号值可以表示与有符号值相同数量的结果。

没错,但这个范围就没那么有用了。考虑减法,范围为0到2N 的无符号数,范围为 -N 到 N 的有符号数,任意减法都会得到范围为 -2N 到2N 的结果,任何一种整数类型都只能表示它的一半。事实证明,以 -N 到 N 的零点为中心的区域通常比0到2N 的范围更有用(在现实世界的代码中包含更多的实际结果)。考虑任何非均匀分布(log、 zipfian、正态分布等)的典型分布,并考虑从该分布中减去随机选择的值: [-N,N ]中的值比[0,2N ]中的值多得多(实际上,结果分布总是集中在零)。

64位关闭了许多使用无符号值作为数字的理由

我认为上面的论点对于32位值已经很有说服力了,但是溢出情况,在不同的阈值下影响有符号和无符号,出现在32位值上,因为“20亿”是一个可以被许多抽象和物理量超越的数字(数十亿美元,数十亿纳秒,数十亿元素的数组)。因此,如果有人对无符号值的正值范围的倍增深信不疑,那么他们就可以证明溢出确实很重要,而且它稍微有利于无符号值。

在专门的域之外,64位值基本上消除了这个问题。签名的64位值有一个9223372036854775807的上限——超过9个 百万亿次。这是很多纳秒(大约292年) ,也是很多钱。它也是一个比任何计算机都要大的阵列,很可能在一个相干的地址空间内存放很长一段时间。所以也许9的10次方对每个人来说都足够了(现在) ?

何时使用无符号值

请注意,样式指南并不禁止甚至不鼓励使用无符号数字,它的结论是:

不要仅仅使用无符号类型来断言变量是非负的。

实际上,无符号变量有很好的用途:

  • 当你不想把一个 N 位数量看作一个整数,而只是一个“位袋”。例如,作为一个位掩码或位图,或 N 布尔值或任何东西。这种使用通常与固定宽度类型(如 uint32_tuint64_t)密切相关,因为您通常想知道变量的确切大小。一个特定变量值得这样处理的提示是,你只能使用诸如 ~|&^>>uint64_t1操作符操作它,而不能使用诸如 +-*uint64_t0等算术操作。

    无符号运算符在这里是理想的,因为按位运算符的行为是定义良好且标准化的。有符号值有几个问题,比如移位时的未定义和未指定行为,以及未指定的表示。

  • 当你真的想要同余关系的时候。有时候你真的需要2 ^ N 的同余关系。在这些情况下,“溢出”是一个特性,而不是一个 bug。无符号值在这里可以得到你想要的,因为它们被定义为使用同余关系。有符号的值根本不能(容易地、有效地)使用,因为它们具有未指定的表示形式,并且溢出未定义。


0.5 在我写完这篇文章之后,我意识到这几乎和我从未见过的 杰罗德的例子是一样的——而且有充分的理由,这是一个很好的例子!

我们在这里讨论的是 size_t,所以通常在32位系统上是2 ^ 32-1,在64位系统上是2 ^ 64-1。

2 在 C + + 中,情况并非如此,因为无符号值在上端比相应的有符号类型包含更多的值,但存在的基本问题是,操作无符号值可能导致(逻辑上)有符号值,但是有符号值没有相应的问题(因为有符号值已经包含无符号值)。

我对谷歌的风格指南有一些经验,也就是很久以前进入公司的糟糕程序员的疯狂指令搭便车指南。这个特别的指导方针只是那本书中几十个古怪规则的一个例子。

只有当您尝试对无符号类型进行算术运算时才会出现错误(参见上面的克里斯 · 乌兹达维尼斯示例) ,换句话说,如果您将它们用作数字的话。无符号类型不是用来存储数量的,而是用来存储 算数,比如容器的大小,它们永远不能是负数,它们可以而且应该用于这个目的。

使用算术类型(如有符号整数)来存储容器大小的想法是愚蠢的。您是否也使用双精度数来存储列表的大小?谷歌有人使用算术类型存储容器大小,并要求其他人做同样的事情,这说明了该公司的一些特点。我注意到这种规定的一点是,他们越是愚蠢,就越需要制定严格的“不做就炒鱿鱼”的规则,否则有常识的人就会忽视这个规则。

使用无符号类型表示非负值..。

  • 更有可能,当使用有符号和无符号值时,会导致涉及类型提升的 bug,正如其他答案深入演示和讨论的那样,但是
  • 不太可能引起错误,涉及选择域能够表示不适当/不允许值的类型。在某些地方,您将假定该值在域中,并且当其他值以某种方式潜入时,可能会出现意想不到且具有潜在危险的行为。

谷歌编码指南强调了第一种考虑。其他指导方针集,比如 C + + 核心指南,更加强调第二点。例如,考虑核心指南 I. 12:

I. 12: 声明一个不能为空的指针作为 not_null

原因

帮助避免解引用 nullptr 错误 避免对 nullptr进行冗余检查。

例子

int length(const char* p);            // it is not clear whether length(nullptr) is valid
length(nullptr);                      // OK?
int length(not_null<const char*> p);  // better: we can assume that p cannot be nullptr
int length(const char* p);            // we must assume that p can be nullptr

通过在源代码中声明意图,实现者和工具可以提供 更好的诊断,例如通过 静态分析,并执行优化,如删除分支 和空测试。

当然,您可以为整数使用 non_negative包装器,这样可以避免这两种类型的错误,但是这也会有其自身的问题..。

谷歌声明是关于使用无符号作为 容器的尺寸类型。相比之下,这个问题似乎更为笼统。当你继续读的时候,请记住这一点。

由于到目前为止大多数答案都是对 google 声明的反应,而对于更大的问题反应较小,我将首先回答关于容器大小为负的问题,然后试图说服任何人(我知道这是没有希望的... ...)相信无符号是好的。

已签署的集装箱尺寸

让我们假设有人编写了一个 bug,这将导致一个负的容器索引。结果要么是未定义行为错误,要么是异常/访问冲突。这真的比索引类型未签名时获得未定义行为或异常/访问冲突更好吗?我想,没有。

现在,有一类人喜欢谈论数学,以及在这种情况下什么是“自然”。一个负数的整型怎么能自然地描述一个本质上 > = 0的东西呢?经常使用大小为负的数组吗?恕我直言,特别是有数学天赋的人会发现这种语义上的不匹配(大小/索引类型表示负数是可能的,而负数大小的数组很难想象)令人恼火。

所以,关于这个问题,唯一的问题就是——正如 google 评论中所说的——编译器是否真的能够积极地帮助找到这样的 bug。甚至比下溢保护的无符号整数更好(x86-64汇编和其他架构可能有实现这一点的方法,只有 C/C + + 不使用这些方法)。我能理解的唯一方法是编译器自动添加运行时检查(if (index < 0) throwOrWhatever) ,或者在编译时操作产生大量潜在的错误正面警告/错误“这个数组访问的索引可能是负面的。”我有疑问,这会有帮助的。

另外,那些实际为数组/容器索引编写运行时检查的人,处理有符号整数是 更多的工作。现在你不需要写 if (index < container.size()) { ... }了,你只需要写: if (index >= 0 && index < container.size()) { ... }。在我看来像是强迫劳动,不像是改善..。

没有无符号类型的语言糟透了..。

Yes, this is a stab at java. Now, I come from embedded programming background and we worked a lot with field buses, where binary operations (and,or,xor,...) and bit wise composition of values is literally the bread and butter. For one of our products, we - or rather a customer - wanted a java port... and I sat opposite to the luckily very competent guy who did the port (I refused...). He tried to stay composed... and suffer in silence... but the pain was there, he could not stop cursing after a few days of constantly dealing with signed integral values, which SHOULD be unsigned... Even writing unit tests for those scenarios is painful and me, personally I think java would have been better off if they had omitted signed integers and just offered unsigned... at least then, you do not have to care about sign extensions etc... and you can still interpret numbers as 2s complement.

这就是我对此事的看法。