使用unsigned而不是signed int更可能导致错误吗?为什么?

use*_*189 77 c c++ google-style-guide

Google C++样式指南中,有关"无符号整数"的主题,建议使用

由于历史意外,C++标准也使用无符号整数来表示容器的大小 - 标准组织的许多成员认为这是一个错误,但在这一点上实际上无法修复.无符号算术不对一个简单整数的行为进行建模,而是通过标准来定义模块化算法(包含溢出/下溢),这意味着编译器无法诊断出一大类错误.

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

指南引用了哪些错误(一个重要的类)?溢出的错误?

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

我可以想到使用signed int而不是unsigned int的一个原因是,如果它确实溢出(为负),则更容易检测.

Bee*_*ope 67

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

根据我的经验,除了混合比较和推广规则之外,无符号值是大错误磁体有两个主要原因.

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

无符号整数和有符号整数都有最小值和最大值的不连续性,它们包围(无符号)或导致未定义的行为(带符号).因为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 }
Run Code Online (Sandbox Code Playgroud)

这有效,直到有一天你传入一个空向量.您将获得v.size() - 1 == a giant number1而不是进行零迭代,而您将进行40亿次迭代,并且几乎有一个缓冲区溢出漏洞.

你需要像这样写:

for (size_t i = 0; i + 1 < v.size(); i++) { // do something }
Run Code Online (Sandbox Code Playgroud)

所以它可以在这种情况下"固定",但只能通过仔细考虑无符号性质size_t.有时你不能应用上面的修复,因为你有一些你想要应用的变量偏移而不是常数,这可能是积极的或消极的:所以你需要把它放在比较的"边"取决于签名- 现在代码变得非常混乱.

尝试迭代到包括零的代码存在类似的问题.类似的while (index-- > 0)工作正常,但显然等效的while (--index >= 0)永远不会终止无符号值.当右侧是字面零时,您的编译器可能会发出警告,但如果它是在运行时确定的值,则肯定不会发出警告.

对位

有些人可能认为签名值也有两个不连续性,那么为什么选择未签名?不同之处在于两个不连续点都非常(最大)远离零.我真的认为这是一个"溢出"的单独问题,有符号和无符号值都可能在非常大的值上溢出.在许多情况下,由于对值的可能范围的限制,溢出是不可能的,并且许多64位值的溢出在物理上是不可能的).即使可能,与"零"错误相比,与溢出相关的错误的机会通常是微不足道的,并且对于无符号值也会发生溢出.所以unsigned结合了两个世界中最糟糕的:可能溢出非常大的幅度值,并且不连续性为零.签名只有前者.

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

即使在未签名的情况下你也会购买:它不会给你带来太大的收益:如果你不得不支持超过20亿的"东西",你可能很快就会支持超过40亿.

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

数学上,无符号值(非负整数)是有符号整数的子集(仅称为_integers).2.然而,有符号的值自然会在无符号值上运行,例如减法.我们可以说无符号值在减法下不会关闭.签名值也是如此.

想要在文件中找到两个未签名索引之间的"delta"吗?那么你最好按正确的顺序进行减法,否则你会得到错误的答案.当然,您经常需要运行时检查以确定正确的顺序!当将无符号值作为数字处理时,您经常会发现(逻辑上)已签名的值仍然会出现,所以您不妨开始使用signed.

对位

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

没错,但范围不太有用.考虑减法,范围为0到2N的无符号数,以及范围为-N到N的带符号数.任意减法在_两种情况下都会产生-2N到2N范围内的结果,并且任何一种整数都只能表示一半.事实证明,以N到N的零为中心的区域通常比0到2N的范围更有用(在现实世界代码中包含更多实际结果).考虑除了均匀(log,zipfian,normal,whatever)之外的任何典型分布,并考虑从该分布中减去随机选择的值:[N,N]中的更多值最终会超过[0,2N](实际上,得到的分布)始终以零为中心).

64位关闭了许多使用签名值作为数字的原因

我认为上面的论点已经引人注目的32位值,但是在不同阈值下影响有符号和无符号的溢出情况确实发生在32位值,因为"20亿"是一个可以被许多人超过的数字抽象和物理量(数十亿美元,数十亿纳秒,数十亿元素的数组).因此,如果某人对无符号值的正范围加倍足够信服,那么他们可以证明溢出确实很重要并且它略微偏向无符号.

在专用域之外,64位值很大程度上消除了这种担忧.符号的64位值具有9,223,372,036,854,775,807上部范围-超过九百万的三次方.那是一个很长的纳秒(大约292年的价值),还有很多钱.它也是一个比任何计算机都要长时间在相干地址空间中拥有RAM的数组.那么对于每个人来说,9个quintillion可能就足够了(现在)?

何时使用无符号值

请注意,样式指南不禁止甚至不一定阻止使用无符号数字.它的结论是:

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

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

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

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

  • 当你真的想要模块化算术.有时你实际上想要2 ^ N模运算.在这些情况下,"溢出"是一个功能,而不是一个错误.无符号值可以为您提供所需的值,因为它们被定义为使用模运算.签名值不能(容易,有效地)使用,因为它们具有未指定的表示并且溢出未定义.

0.5在我写完之后,我意识到这几乎与Jarod的例子相同,我没有看到 - 而且有充分的理由,这是一个很好的例子!

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

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

  • 我同意你发布的所有内容,但"64位对每个人来说应该足够了"肯定似乎太接近"640k应该足以满足每个人". (10认同)
  • @Andrew - 是的,我仔细选择了我的话:). (6认同)
  • "64位关闭无符号值的大门" - >不同意.一些整数编程任务很简单,不是计数的情况,也不需要负值但需要2的幂宽度:密码,加密,位图形,无符号数学的好处.这里的许多想法都指出了为什么代码可以在有能力的时候使用带符号的数学,但却没有使_unsigned_类型无用并关闭它们的门. (4认同)
  • @Deduplicator - 是的,我把它留了出来,因为它似乎或多或少像一条领带.在unsigned mod-2 ^ N环绕的一侧,你至少有一个已定义的行为,并且没有意外的"优化"会启动.在UB一侧,在无符号或有符号的算术运算期间的任何溢出可能是绝大多数的错误案例(在少数人期望模拟算术之外)和编译器提供的选项如`-ftrapv`可以捕获所有已签名的溢出,但不是所有无符号溢出.性能影响也不算太差,因此在某些情况下使用`-ftrapv`进行编译可能是合理的. (2认同)
  • @BeeOnRope`这是关于以纳秒为单位测量的宇宙年龄.我对此表示怀疑.宇宙大约是"13.7*10 ^ 9年",即"4.32*10 ^ 17 s"或"4.32*10 ^ 26 ns".要将"4.32*10 ^ 26"表示为int,您至少需要"90位".`9,223,372,036,854,775,807 ns`仅约为'292.5年'. (2认同)

Jar*_*d42 34

如上所述,混合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
Run Code Online (Sandbox Code Playgroud)

假设v.size() < 5,那么,作为v.size()IS unsigned,s.size() - 5将是一个非常大的数字,因此i < v.size() - 5将是true对的价值更预期范围i.然后UB很快发生(一次出界i >= v.size())

如果v.size()将返回有符号值,那么s.size() - 5将是负数,并且在上面的情况下,条件将立即为假.

在另一边,指数应该介于[0; v.size()[这样unsigned才有意义.签名也有自己的问题作为UB,具有溢出或实现定义的行为,用于负向签名号码的右移,但是迭代的错误来源较少.

  • 你所说的基本上是必须知道类型之间的语言和强制规则.我不知道这是如何改变是否使用签名或无符号问题.如果不需要负值,我建议不要使用signed.我同意@geza,只在必要时才使用_signed_.这使得google指南充满疑问**.Imo这是一个糟糕的建议. (8认同)
  • @PasserBy:如果你称他们为奥术,你必须添加整数提升和UB以获得签名类型的溢出.并且非常常见的sizeof运算符无论如何返回unsigned,所以你必须**知道它们.说:如果你不想学习语言细节,就不要使用C或C++!考虑到谷歌推广去,也许这正是他们的目标."不要做坏事"的日子早已不复存在...... (5认同)
  • 虽然我自己使用带符号的数字,但我认为这个例子不够强大.一个人长时间使用无符号数字,肯定知道这个成语:而不是`i <size() - X`,应该写`i + X <size()`.当然,这是一件值得记住的事情,但在我看来,这并不难以适应. (2认同)
  • @toohonestforthissite关键是规则是神秘的,沉默的和主要的错误原因.使用专门签名的算法类型可以解决问题.BTW使用无符号类型来强制执行正值是对他们最严重的滥用之一. (2认同)
  • 值得庆幸的是,现代编译器和IDE在表达式中混合有符号和无符号数时会发出警告. (2认同)

Chr*_*nis 19

当您修改有符号和无符号值时,最令人头疼的错误示例之一是:

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

输出:

世界没有意义

除非你有一个简单的应用程序,否则你最终会遇到有符号和无符号值之间的危险混合(导致运行时错误),或者如果你发出警告并使它们成为编译时错误,你最终会得到很多代码中的static_casts.这就是为什么最好严格使用有符号整数进行数学或逻辑比较的类型.仅对位掩码和表示位的类型使用无符号.

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

  • _如果不将隐式转换视为警告并且过于懒惰而无法使用正确的类型转换,则根据数字值的预期域对未编签的类型进行编码是一个坏主意*.*根据预期对类型进行建模有效值是完全合理的,只是没有内置类型的C/C++. (8认同)
  • 一旦你的代码库混合了有符号值和无符号值,当你发出警告并将它们提升为错误时,代码最终会乱丢static_casts以使转换显式化(因为数学仍然需要完成.)即使是正确的,它容易出错,难以使用,难以阅读. (2认同)

chu*_*ica 11

为什么使用unsigned int比使用signed int更容易导致错误?

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

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

模运算有什么问题?这不是unsigned int的预期行为吗?
为什么使用unsigned int比使用signed int更容易导致错误?

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

安全性,加密和身份验证算法依赖于无符号模块化数学.

压缩/解压缩算法以及各种图形格式都可以获益,并且无需使用无符号数学就可以减少错误.

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


有符号整数数学具有直观的外观,所有人都可以理解,包括学习者编码.C/C++最初没有被定位,现在也不应该是一个介绍语言.对于采用溢流安全网的快速编码,其他语言更适合.对于精简快速代码,C假设编码员知道他们在做什么(他们经验丰富).

今天签名数学的一个陷阱是无处不在的32位int,如此多的问题对于没有范围检查的常见任务来说已经足够广泛了.这导致了溢出不被编码的自满情绪.相反,for (int i=0; i < n; i++) int len = strlen(s);被认为是好的因为n假设< INT_MAX并且字符串永远不会太长,而不是在第一种情况下或使用size_t,unsigned甚至long long在第二种情况下被全范围保护.

C/C++是在一个包含16位和32位的时代开发的,int而无符号16位的额外位size_t是非常重要的.需要关于注意溢出问题是它int还是unsigned.

在非16位int/unsigned平台上使用32位(或更广泛)的Google应用程序,在int给定其充足范围的情况下,缺乏对+/-溢出的关注.这是有道理的,这样的应用,鼓励intunsigned.然而,int数学没有得到很好的保护

int/unsigned如今,狭窄的16位问题适用于精选的嵌入式应用.

Google的指南适用于他们今天编写的代码.它不是更广泛的C/C++代码范围的权威指南.


我可以想到使用signed int而不是unsigned int的一个原因是,如果它确实溢出(为负),则更容易检测.

在C/C++中,signed int math overflow是未定义的行为,因此不一定比无符号数学的定义行为更容易检测.


正如@Chris Uzdavinis所评论的那样,所有人(特别是初学者)最好避免使用有符号无符号的混合,并在需要时仔细编码.

  • 你明确指出`int`不会模拟"实际"整数的行为.溢出时未定义的行为不是数学家对整数的看法:它们不可能用抽象整数"溢出".但这些是机器存储单元,而不是数学家的数字. (2认同)

Tyl*_*den 5

我有一些谷歌风格指南的经验,AKA漫画指导者来自那些长时间进入公司的糟糕程序员的疯狂指令.这个特殊的指导原则只是该书中几十个坚果规则的一个例子.

如果您尝试对它们进行算术运算,则只会出现无符号类型的错误(请参阅上面的Chris Uzdavinis示例),换句话说,如果您将它们用作数字.无符号类型不用于存储数字量,它们用于存储诸如容器大小之类的计数,这些计数永远不会是负数,并且它们可以而且应该用于此目的.

使用算术类型(如有符号整数)来存储容器大小的想法是愚蠢的.你会用双倍来存储列表的大小吗?谷歌有人使用算术类型存储容器大小并要求其他人做同样的事情说明公司的事情.关于这些要求,我注意到的一件事是,他们的笨拙,他们越需要严格执行或者你被解雇的规则,否则具有常识的人会忽视这一规则.

  • 计数通常与对其进行算术运算的事物(例如索引)进行比较。C 处理涉及有符号和无符号数字的比较的方式可能会导致许多奇怪的怪癖。除了计数的最高值适合无符号类型但不适合相应的有符号类型的情况(在“int”为 16 位的时代很常见,但现在少得多),最好让计数表现得像数字。 (2认同)
  • “只有当您尝试对无符号类型进行算术运算时,才会出现错误” - 这种情况经常发生。“使用算术类型(如带符号整数)来存储容器大小的想法是愚蠢的” - 事实并非如此,C++ 委员会现在认为使用 size_t 是一个历史错误。原因?隐式转换。 (2认同)