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

时间:2018-08-03 17:57:12

标签: c++ c google-style-guide

Google C++ Style Guide的“无符号整数”主题中,建议

  

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

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

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

  

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

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

7 个答案:

答案 0 :(得分:65)

这里的一些答案提到了有符号和无符号值之间令人惊讶的提升规则,但这似乎更像是与混合有符号和无符号值有关的问题,并不一定解释为什么 signed 比 unsigned 更为可取。

根据我的经验,除了混合比较和升级规则外,无符号值是吸引大错误的两个主要原因。

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

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

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

考虑的代码按索引遍历向量中的所有值,除了last 0.5

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

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

您需要这样写:

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

因此,在这种情况下可以对其进行“修复”,但必须仔细考虑size_t的无符号性质。有时您无法应用上面的修复程序,因为您没有要应用的固定偏移量,而是要应用一些可变偏移量,该偏移量可以是正数或负数:因此,您需要进行比较的哪一面取决于有符号性-现在代码变得真的混乱了。

代码中有一个类似的问题,它试图迭代到零并包括零。像while (index-- > 0)这样的东西可以正常工作,但显然等效的while (--index >= 0)永远不会因无符号值而终止。当右侧 literal 为零时,编译器可能会警告您,但如果它是在运行时确定的值,则肯定不会警告您。

对等点

有人可能会认为带符号的值也有两个不连续之处,那么为什么选择不带符号的呢?不同之处在于两个不连续点都非常(最大)远离零。我真的认为这是一个单独的“溢出”问题,有符号和无符号值都可能在很大的值上溢出。在很多情况下,由于值的可能范围的限制,不可能发生溢出,并且许多64位值的溢出在物理上可能是不可能的)。即使可能,与“零”错误相比,与溢出相关的错误的可能性也通常很小,并且对于无符号值也会发生溢出。因此,无符号结合了两种情况中最糟糕的情况:潜在的溢出具有非常大的幅度值,并且不连续为零。只有前者签名。

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

即使在未签名的情况下可以买到您的东西:它也买不了很多:如果您必须支持超过20亿个“事物”,那么您可能很快就会不得不支持超过40亿。 >

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

在数学上,无符号值(非负整数)是有符号整数(仅称为_integers)的子集。 2 。然而, signed 值自然会仅对 unsigned 值(例如减法)弹出操作。我们可以说未加符号的值在减法中不是 closed 。签名值并非如此。

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

对等点

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

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

使用位符号值作为数字的许多原因使64位闭门

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

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

何时使用无符号值

请注意,样式指南不会禁止甚至禁止使用无符号数字。结论如下:

  

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

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

  • 当您不希望将N位数量视为整数时,而只是将其视为“位包”。例如,作为位掩码或位图,或N个布尔值或其他值。这种用法通常与固定宽度类型(例如uint32_tuint64_t)并存,因为您经常想知道变量的确切大小。提示某个变量值得此处理的提示是,您只能使用 bitwise 运算符对其进行操作,例如~|&,{{ 1}},^等,而不是>>+-*等算术运算。

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

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

0.5 在我写完这篇文章之后,我意识到这与Jarod's example几乎完全相同,但我从未见过-出于充分的原因,这是一个很好的例子!

1 我们在这里谈论/的原因通常是在32位系统上为2 ^ 32-1,在64位系统上为2 ^ 64-1。

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

答案 1 :(得分:33)

如上所述,混合使用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将为{{1} }的期望值范围true。然后,UB很快就会发生(一次i就会出现绑定访问)

如果i >= v.size()将返回带符号的值,则v.size()将为负,并且在上述情况下,条件将立即为假。

另一方面,索引应该在s.size() - 5之间,因此[0; v.size()[才有意义。 Signed也有其自身的问题,即UB,它具有溢出或实现定义的行为,用于负号负数的右移,但迭代错误的发生频率较低。

答案 2 :(得分:19)

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

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

输出:

世界没有道理

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

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

答案 3 :(得分:11)

  

为什么使用无符号整数比使用有符号整数更容易导致错误?

signed 类型用于某些任务类别相比,使用 unsigned 类型引起错误的可能性较小。

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

  

模块化算术出了什么问题?这不是unsigned int的预期行为吗?
  为什么使用无符号整数比使用有符号整数更容易导致错误?

如果任务匹配良好:没错。不,不太可能。

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

压缩/解压缩算法以及各种图形格式也受益匪浅,而且使用 unsigned 数学时,它们的bug更少。

每当使用按位运算符和移位时, unsigned 操作都不会陷入 signed 数学的符号扩展问题。


有符号整数数学具有直观的外观,并为包括编码学习者在内的所有人所理解。 C / C ++最初并不是针对性的,现在也不应该是入门语言。对于使用涉及溢出的安全网的快速编码,更适合使用其他语言。对于精简快速代码,C假定编码人员知道他们在做什么(他们是有经验的)。

如今, signed 数学的一个陷阱是无处不在的32位int,它具有如此多的问题,足以解决不进行范围检查的常见任务。这导致不对溢出进行编码的自满情绪。相反,for (int i=0; i < n; i++) int len = strlen(s);被认为是可以的,因为假设n <INT_MAX,并且字符串永远不会太长,而不是在第一种情况下或使用时受到全范围保护size_tunsigned甚至第二个long long

在一个包含16位和32位int的时代中开发的C / C ++,而无符号16位size_t提供的额外位意义重大。需要注意intunsigned上的溢出问题。

在非16位int/unsigned平台上使用Google的32位(或更广泛的)应用程序,由于其范围足够大,因此没有引起对int +/-溢出的关注。对于此类应用而言,鼓励int胜过unsigned是有意义的。但是int数学没有得到很好的保护。

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

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


  

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

在C / C ++中,有符号的int数学溢出是未定义的行为,因此,与确定的 unsigned 数学的行为相比,它肯定不容易检测。


正如@Chris Uzdavinis所言,所有(尤其是初学者)最好避免混合使用 signed unsigned ,并在需要时进行仔细编码。

答案 4 :(得分:5)

我对Google的风格指南(也就是长期以来进入公司的不良程序员的Hitchhiker疯狂指令指南)有所了解。该特定准则只是该书中数十种坚果规则的一个示例。

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

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

答案 5 :(得分:1)

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

  • 更有可能在使用带符号和无符号值时引起涉及类型提升的错误,其他答案对此进行了深入的论证和讨论,但是
  • 不太可能会引起涉及类型选择的错误,这些错误的域具有能够代表不希望的/不允许的值的域。在某些地方,您会假定该值在域中,并且当其他值以某种方式潜入时,可能会导致意外的和潜在的危险行为。

Google编码指南将重点放在第一种考虑上。其他准则集,例如C++ Core Guidelines,则更加强调第二点。例如,考虑核心准则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整数包装器,这样可以避免两种类型的错误,但是会产生自己的问题...

答案 6 :(得分:0)

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

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

签名的容器大小<​​/ h3>

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

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

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

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

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

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

这是我的5美分。