使用无符号而不是带符号的int更有可能导致错误吗?为什么?


81

在《Google C ++样式指南》的“无符号整数”主题上,建议

由于历史原因,C ++标准还使用无符号整数来表示容器的大小-标准主体的许多成员认为这是一个错误,但实际上目前无法修复。无符号算术不对简单整数的行为进行建模,而是由标准定义以对模块化算术进行建模(围绕上溢/下溢进行环绕),这一事实意味着编译器无法诊断出大量的错误。

模块化算法有什么问题?这不是unsigned int的预期行为吗?

该指南指的是哪种错误(重要的类)?错误溢出?

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

我可以想到在未签名的int上使用有符号的int的原因之一是,如果它确实溢出(变为负数),则更易于检测。


4
尝试去做unsigned int x = 0; --x;,看看会x变成什么样。如果没有限制检查,大小可能突然获得一些意外值,很容易导致UB。
一些程序员花花公子

33
至少无符号溢出具有明确定义的行为并产生预期的结果。
user7860670

35
在不相关的问题上(与您的问题无关,但与Google样式指南无关),如果稍加搜索,就会发现对Google样式指南的某些批评(有时是正确的)。不要把它们当作福音。
一些程序员伙计

18
另一方面,上int溢和下溢都是UB。与int将值减unsigned int为零以下的情况相比,您不太可能遇到试图表达其无法表达的值的情况,但是那种会对unsigned int算术行为感到惊讶的人就是那种也可能编写会导致int与溢出相关的UB的代码,例如a < a + 1用于检查溢出。
弗朗索瓦·安德里厄

12
如果无符号整数溢出,则定义明确。如果有符号整数溢出,则它是不确定的行为。我更喜欢定义良好的行为,但是如果您的代码无法处理溢出的值,那么两者都会迷失方向。区别在于:对于已签名,您已经为溢出操作迷失了,对于以下代码中的未签名,您已经迷失了。我唯一同意的一点是,如果需要负值,则无符号整数类型是错误的选择-显然。
对于这个网站来说太老实了

Answers:


70

一些问题的答案在这里提到符号和无符号值之间的令人惊讶的促销规则,但这似乎更像是有关问题混合符号和无符号值,并且不一定解释为什么签署的变量会优先于无符号混合的场景之外。

以我的经验,除了混合比较和升级规则外,导致未签名的值成为吸引用户的原因有两个主要原因,如下所示。

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

无符号整数和有符号整数在其最小值和最大值处均具有不连续性,它们在其中环绕(无符号)或导致未定义的行为(有符号)。因为unsigned这些点在UINT_MAX。因为int他们在INT_MININT_MAX。的典型值INT_MININT_MAX与4字节的系统int的值是-2^312^31-1,并且这样的系统上UINT_MAX通常是2^32-1

导致错误的主要问题unsigned不适用于int它的不连续性为零。零当然是程序中非常常见的值,例如1,2,3等其他小值。通常在各种结构中添加和减去较小的值,尤其是1,并且如果从一个unsigned值中减去任何值并且恰好为零,那么您将得到一个巨大的正值和一个几乎确定的错误。

考虑代码对索引中除最后0.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位值的溢出。即使可能,与“零”错误相比,与溢出相关的错误的可能性也通常很小,并且对于无符号值也会发生溢出。因此,无符号结合了两种情况中最糟糕的情况:潜在的溢出具有非常大的幅度值,并且不连续为零。签名只有前者。

许多人会因未签名而争辩“您输了一点”。这通常是正确的-但并非总是如此(如果您需要表示无符号值之间的差异,则无论如何都会丢失该位:无论如何,如此多的32位内容仅限于2 GiB,或者您会在其中说一个奇怪的灰色区域一个文件可以是4 GiB,但不能在后2个GiB的一半上使用某些API)。

即使在未签名的情况下会给您带来一点好处:它也不会给您带来多少好处:如果您必须支持超过20亿个“事物”,那么您可能很快就会不得不支持超过40亿个。

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

从数学上讲,无符号值(非负整数)是有符号整数(仅称为_integers)的子集。2。然而,有符号的值自然会从仅对无符号值(例如减法)的运算中弹出。我们可能会说未签名的值不会在减法后关闭。带符号的值并非如此。

是否想在文件的两个无符号索引之间找到“增量”?好吧,您最好按正确的顺序进行减法运算,否则您将得到错误的答案。当然,您通常需要运行时检查以确定正确的顺序!当将无符号值作为数字处理时,您经常会发现(逻辑上)带符号的值始终会出现,因此最好也从带符号开始。

对点

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

是的,但是范围用处不大。请考虑减法和0到2N范围内的无符号数,以及-N到N范围内的有符号数。在_both和bth情况下,任意减法都会导致-2N到2N范围内的结果,并且任何一种整数只能表示一半。事实证明,以-N到N的零为中心的区域通常比0到2N范围更有用(在现实世界代码中包含更多实际结果)。考虑均匀分布以外的任何典型分布(对数,zipfian,正态分布等),并考虑从该分布中减去随机选择的值:以[-N,N]结尾的值多于[0,2N](实际上是结果分布)始终以零为中心)。

由于使用带符号的值作为数字的许多原因,64位关闭了大门

我认为上面的论点对于32位值已经很有说服力,但是对于32位值,确实发生了影响不同阈值的有符号和无符号的溢出情况,因为“ 20亿”这个数字可以被许多人超过抽象和物理量(数十亿美元,数十亿纳秒,包含数十亿个元素的数组)。因此,如果有人对无符号值的正范围加倍有足够的信心,那么他们可以说溢出确实很重要,并且它偏向于无符号。

在专用域之外,64位值在很大程度上消除了这种担忧。有符号的64位值的上限范围为9,223,372,036,854,775,807-超过九位数。那是很多纳秒(约292年的价值),而且很多钱。它也是一个更大的阵列,比任何计算机都可能在很长一段时间内在一致的地址空间中拥有RAM更大。那么,对于每个人来说(现在)9位数就足够了吗?

何时使用无符号值

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

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

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

  • 当您不希望将N位数量视为整数时,而只是将其视为“位包”。例如,作为位掩码或位图,或N个布尔值或其他值。这种用法通常与固定宽度类型(例如uint32_t和)结合使用,uint64_t因为您经常想知道变量的确切大小。一个特定的变量配得上这个治疗的暗示是,你只能在它与操作按位运算符,如~|&^>>等等,而不是与算术操作,如+-*/等。

    在这里,无符号是理想的,因为按位运算符的行为是定义明确和标准化的。带符号的值有几个问题,例如移位时的不确定行为和不确定的表示形式以及不确定的表示形式。

  • 当您实际需要模块化算术时。有时您实际上需要2 ^ N模块化算术。在这些情况下,“溢出”是功能而不是错误。由于将无符号值定义为使用模块化算术,因此它们可为您提供所需的信息。不能完全(轻松,有效地)使用带符号的值,因为它们具有未指定的表示形式并且未定义溢出。


0.5写完这篇文章后,我意识到这与Jarod的示例几乎完全相同,但我从未见过-出于充分的理由,这是一个很好的示例!

1我们在size_t这里谈论的通常是32位系统上的2 ^ 32-1或64位系统上的2 ^ 64-1。

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


10
我同意您所发布的所有内容,但是“ 64位应该对每个人都足够”肯定会太接近“ 640k应该对每个人都足够”。
Andrew Henle

6
@Andrew-是的,我仔细选择了我的话:)。
BeeOnRope

4
“ 64位关闭无符号值的大门”->不同意。一些整数编程任务很简单,而不是计数,不需要负值,但需要2的幂次幂:密码,加密,位图,无符号数学带来的好处。这里有许多想法指出为什么代码可以使用带符号的数学运算,但是却使无符号类型变得无用并关闭了它们的大门。
chux-恢复莫妮卡

2
@Deduplicator-是的,我忽略了它,因为它看起来或多或少像一条领带。在无符号mod-2 ^ N包装的一侧,您至少具有已定义的行为,并且不会出现意外的“优化”。在UB一侧,在无符号或有符号的算术期间的任何溢出都可能是绝大多数错误(少数人希望采用mod算术),并且编译器提供了类似的选项-ftrapv,可以捕获所有有符号的溢出,但不能捕获所有无符号的溢出。对性能的影响还不错,因此-ftrapv在某些情况下进行编译可能是合理的。
BeeOnRope

2
@BeeOnRopeThat's about the age of the universe measured in nanoseconds.我对此表示怀疑。宇宙是关于或的13.7*10^9 years古老。表示为int至少需要。只会约。4.32*10^17 s4.32*10^26 ns4.32*10^2690 bits9,223,372,036,854,775,807 ns292.5 years
奥西里斯(Osiris)'18年

36

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

假设您要遍历vector的所有元素(最后五个除外),则可能会写错:

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()unsigneds.size() - 5将是一个非常大的数字,因此i < v.size() - 5将是true对的值更的预期范围i。然后,UB快速发生(一次无界访问i >= v.size()

如果v.size()将返回带符号的值,则将为s.size() - 5负,在上述情况下,条件将立即为假。

另一方面,索引应该介于两者之间,[0; v.size()[这样unsigned才有意义。Signed也有其自身的问题,即UB,它具有溢出或实现定义的行为,用于向右移动负号,但迭代的错误源较少。


2
尽管我本人会尽可能使用带符号的数字,但我认为此示例不够强大。长时间使用无符号数字的人肯定知道这个成语:而不是i<size()-X应该写i+X<size()。当然,这是要记住的事情,但在我看来,习惯并不难。
geza,

8
您所说的基本上是必须了解类型之间的语言和强制性规则。我看不出这个问题是如何使用签名或未签名的。并不是说,如果不需要负值,我建议根本不使用带符号。我同意@geza,仅在必要时使用签名。这使Google指南充其量是可疑。伊莫这是个坏建议。
对于这个网站来说太老实了

2
@toohonestforthissite关键是规则是奥秘的,无声的和错误的主要原因。使用专门签名的类型进行算术可以减轻您的麻烦。BTW使用无符号类型强制执行正值是对它们的最严重滥用之一。
过客

2
幸运的是,当在表达式中混合带符号和无符号数字时,现代编译器和IDE会发出警告。
Alexey B.

5
@PasserBy:如果您将其称为arcane,则还必须添加整数提升和UB,以使签名类型的arcane溢出。而且非常常见的sizeof运算符无论如何都会返回一个无符号,因此您必须了解它们。说:如果您不想学习语言细节,那就不要使用C或C ++!考虑到Google推广go,也许正是他们的目标。“别作恶”的日子早已荡然无存……
对于这个网站来说太老实了

20

最令人毛骨悚然的错误示例之一是MIX带符号和无符号值:

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

输出:

世界没有道理

除非您有一个琐碎的应用程序,否则不可避免地会导致在有符号值和无符号值之间进行危险的混合(导致运行时错误),或者如果您增加警告并使其成为编译时错误,则最终会导致很多代码中的static_casts。这就是为什么最好对数学或逻辑比较类型的类型严格使用带符号整数。仅将unsigned用于位掩码和表示位的类型。

根据数字值的预期域为无符号类型建模是一个坏主意。大多数数字比20亿更接近0,因此对于无符号类型,许多值更接近有效范围的边缘。更糟的是,最终值可能在已知的正范围内,但是在评估表达式时,中间值可能会下溢,并且如果以中间形式使用它们,则可能是非常错误的值。最后,即使期望您的值始终为正,也并不意味着它们将不会与其他可能为负的变量发生交互,因此您最终不得不将有符号和无符号类型混合在一起,这是最糟糕的地方。


8
建模类型根据您的数字值的预期域是无符号是一个坏主意*如果你不把隐式转换为警告,都懒得使用正确的类型转换。*在他们的预期有效建模的类型值是完全合理的,只是在C / C ++中没有内置类型。
villasv

1
@ user7586189这是一个好的做法,使无效数据无法实例化,因此为大小设置仅正变量是完全合理的。但是您不能微调C / C ++内置类型,以默认情况下不允许像此答案中的那样强制转换,并且有效性最终由其他人负责。如果您使用的语言具有更严格的转换(即使是内置的转换),则预期域建模是一个不错的主意。
villasv

1
请注意,我确实提到了提高警告并将其设置为错误,但并非所有人都这样做。我仍然不同意@villasv与您有关建模值的声明。通过选择unsigned,您还隐式地建模了可能与之联系的所有其他值,而没有太多预见性。几乎可以肯定会弄错。
克里斯·乌兹达维尼斯

1
考虑领域建模是一件好事。使用unsigned建模域不是。(签名VS无符号应根据类型进行选择使用,而不是一系列的,除非它是不可能不这样做。)
克里斯Uzdavinis

2
一旦您的代码库混合了带符号和无符号的值,当您打开警告并将其提升为错误时,代码最终会充满static_casts以使转换明确(因为数学仍然需要完成。)即使正确,它容易出错,更难以使用,也更难以阅读。
克里斯·乌兹达维尼斯

11

为什么使用无符号int比使用有符号int更有可能导致错误?

使用一个无符号的类型是不是更容易造成错误比使用签名的类型与某些类型的任务。

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

模块化算法有什么问题?这不是unsigned int的预期行为吗?
为什么使用无符号int比使用有符号int更有可能导致错误?

如果任务相配:没有错。不,可能性不大。

安全性,加密和身份验证算法依靠无符号的模块化数学。

压缩/解压缩算法以及各种图形格式也受益匪浅,而且无符号数学的错误率也较低。

位运算符和班次,使用的任何时间,无符号运算没有得到搞砸了的符号扩展问题签署了数学。


有符号整数数学具有直观的外观,并为包括编码学习者在内的所有学习者轻易理解。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平台上使用Google的32位(或更广泛的)应用程序时,您无需关注+/-溢出int由于其范围足够大。对于这样的应用程序,鼓励int过度使用是有意义的unsigned。然而int数学并没有得到很好的保护。

狭窄的16位int/unsigned问题今天适用于某些嵌入式应用程序。

Google的准则非常适用于他们今天编写的代码。对于较大范围的C / C ++代码,它不是确定的准则。


我可以想到在未签名的int上使用有符号的int的原因之一是,如果它确实溢出(变为负数),则更易于检测。

在C / C ++,有符号整数数学溢出是未定义的行为,因此不容易肯定比定义的行为来检测无符号数学的。


正如@Chris Uzdavinis所指出的那样,所有人(尤其是初学者)最好避免混合使用带符号无符号,并在需要时仔细进行编码。


2
您提出了一个很好的观点,即int也不会对“实际”整数的行为进行建模。溢出时未定义的行为不是数学家如何看待整数的:它们不可能与抽象整数“溢出”。但是这些是机器存储单元,而不是数学家的数字。
tchrist

1
@tchrist:溢出时的无符号行为是数学家如何考虑整数全等mod(type_MAX + 1)的抽象代数环。
超级猫

如果您使用的是gcc,signed int则很容易检测到溢出(带有-ftrapv),而难以检测到无符号的“溢出”。
anatolyg

5

我对Google的风格指南有一些经验,也就是很久很久以前就进入公司的不良程序员的《搭便车的疯狂指令指南》。该特定指南只是该书中数十种坚果规则的一个示例。

仅当您尝试对无符号类型进行算术运算时才会发生错误(请参见上面的Chris Uzdavinis示例),换句话说,如果将它们用作数字,则会发生错误。无符号类型无意用于存储数字量,它们无意存储诸如容器大小之类的计数,它们永远不能为负,它们可以并且应该用于该目的。

使用算术类型(如带符号整数)来存储容器大小的想法是愚蠢的。您还会使用双精度来存储列表的大小吗?Google中有人用算术类型存储容器大小,并要求其他人也做同样的事情,这说明了该公司。我注意到这样的指示的一件事是,他们是愚蠢的人,他们越需要严格执行“按需执行”规则,因为否则常识性的人会忽略该规则。


当我不知所措时,如果unsigned类型只能保存计数并且不能用于算术运算,则执行毯式语句实际上将消除按位运算。因此,“来自不良程序员的疯狂指令”部分更有意义。
David C. Rankin

@ DavidC.Rankin请不要将其视为“空白”声明。显然,无符号整数有多种合法用途(例如存储按位值)。
泰勒·德登

是的,是的-我没有,这就是为什么我说“我明白了”。
David C. Rankin

1
通常将计数与对其进行算术运算的事物(例如索引)进行比较。C处理涉及有符号和无符号数字的比较的方式可能导致许多奇怪的怪癖。除非计数的最高值适合无符号但不适合相应的带符号类型(在int16位的日子很常见,而今天却很少),最好让计数的行为像数字一样。
超级猫

1
“如果尝试对无符号类型进行运算,则错误只会发生在无符号类型上”-经常发生。“使用算术类型(如带符号整数)来存储容器大小的想法是愚蠢的”-事实并非如此,C ++委员会现在认为使用size_t是一个历史错误。原因?隐式转换。
阿提拉·内维斯

1

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

  • 更可能涉及类型的推广,使用符号和无符号值时,其他的答案展示和深入探讨,引起的错误,但
  • 不太可能涉及的类型的选择与能够表示undersirable /不允许值的域原因的错误。在某些地方,您会假设该值在域中,并且当其他值以某种方式潜入时,可能会发生意外的潜在危险行为。

《 Google编码指南》强调第一种考虑。其他准则集,例如C ++核心准则,则更着重于第二点。例如,考虑核心准则I.12

I.12:声明一个不能为null的指针,因为 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整数包装器,这样可以避免两种类型的错误,但是会有其自身的问题...


0

google语句是关于将unsigned用作容器的大小类型。相反,这个问题似乎更笼统。在继续阅读时,请记住这一点。

由于到目前为止,大多数答案都是对google语句做出的反应,对于较大的问题则是较少的回答,因此,我将开始就负容器大小进行回答,然后尝试说服任何人(绝望,我知道...)未签名是好的。

签名的容器尺寸

假设有人编码了一个错误,导致容器索引为负。结果是未定义的行为或异常/访问冲突。这真的比未定义索引类型时获得未定义的行为或异常/访问冲突好吗?我觉得不行。

现在,有一类人喜欢谈论数学以及在这种情况下什么是“自然的”。具有负数的整数类型如何自然地描述本质上大于等于0的事物?使用负大小的数组多少?恕我直言,特别是在数学上偏爱的人会发现这种语义上的不匹配(大小/索引类型表示负数是可能的,而负数大小的数组很难想象)会令人烦恼。

因此,唯一的问题是,如google评论中所述,编译器是否实际上可以积极协助发现此类错误。而且甚至比替代方法更好,后者将是受下溢保护的无符号整数(x86-64汇编和可能的其他体系结构都具有实现此目标的方法,只有C / C ++不会使用这些方法)。我能理解的唯一方法是,如果编译器自动添加了运行时检查(if (index < 0) throwOrWhatever),或者在编译时操作产生大量潜在的错误肯定警告/错误“此数组访问的索引可能为负”。我对此表示怀疑,这会有所帮助。

另外,实际编写运行时的人员会检查其数组/容器索引,这是处理带符号整数的更多工作。if (index < container.size()) { ... }现在您无需编写,而只需编写:if (index >= 0 && index < container.size()) { ... }。对我来说看起来像是强迫劳动,而不是改善。

没有无符号类型的语言很烂...

是的,这是java的一个刺。现在,我来自嵌入式编程背景,我们与现场总线合作了很多,其中二进制运算(和,或,xor,...)和按位组成的值实际上就是面包。对于我们的一种产品,我们-或更确切地说是客户-想要一个Java端口...而我和做该端口的幸运的,非常称职的家伙坐在对面(我拒绝了...)。他试图保持镇静...并默默忍受...但是痛苦在那里,在持续处理有符号整数值几天后,他就不能停止咒骂,这个整数值应该是无符号的...甚至为这些场景让我很痛苦,就我个人而言,如果Java省略了有符号整数而只提供了无符号整数,那我本来会更好。至少在那时,您不必关心符号扩展等。

那是我的5美分。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.