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

Is using an unsigned rather than signed int more likely to cause bugs? Why?

Google C++ Style Guide中,关于"Unsigned Integers"的话题,建议

Because of historical accident, the C++ standard also uses unsigned integers to represent the size of containers - many members of the standards body believe this to be a mistake, but it is effectively impossible to fix at this point. The fact that unsigned arithmetic doesn't model the behavior of a simple integer, but is instead defined by the standard to model modular arithmetic (wrapping around on overflow/underflow), means that a significant class of bugs cannot be diagnosed by the compiler.

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

该指南提到了哪些错误(重要 class)?错误溢出?

Do not use an unsigned type merely to assert that a variable is non-negative.

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

如前所述,混合使用 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() - 5true 以获得更预期的值范围 i。然后 UB 发生得很快(一次越界访问 i >= v.size()

如果 v.size() 将具有 return 有符号值,则 s.size() - 5 将为负数,在上述情况下,条件将立即为假。

另一方面,索引应该在 [0; v.size()[ 之间,所以 unsigned 有意义。 Signed 也有其自身的问题,因为 UB 具有溢出或实现定义的负符号数右移行为,但迭代错误的来源较少。

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

#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 值可能在一个已知的正范围内,但在计算表达式时,中间值可能会下溢,如果它们以中间形式使用,则可能是非常错误的值。最后,即使你的价值观总是正数,这并不意味着它们不会与 其他 变量相互作用 可以 负数,所以你最终会被迫混合有符号和无符号类型,这是最糟糕的地方。

Why is using an unsigned int more likely to cause bugs than using a signed int?

使用 unsigned 类型不会比对某些 类 任务使用 signed 类型更容易导致错误.

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

What is wrong with modular arithmetic? Isn't that the expected behaviour of an unsigned int?
Why is using an unsigned int more likely to cause bugs than using a signed int?

如果任务匹配得当:没有错。不,不太可能。

安全、加密和身份验证算法依赖于未签名的模块化数学。

Compression/decompression 算法以及各种图形格式也受益于 unsigned 数学。

任何时候使用按位运算符和移位,unsigned 操作都不会与 signed[=74] 的符号扩展问题混淆=] 数学。


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

今天 signed 数学的一个陷阱是无处不在的 32 位 int,它有这么多问题,对于没有范围检查的常见任务来说已经足够宽了。这导致对溢出没有编码的自满情绪。相反,for (int i=0; i < n; i++) int len = strlen(s); 被视为 OK,因为假定 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 问题适用于今天的 select 嵌入式应用程序。

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


One reason that I can think of using signed int over unsigned int, is that if it does overflow (to negative), it is easier to detect.

在 C/C++ 中,signed int 数学溢出是 未定义的行为,因此肯定不会比 unsigned[= 的已定义行为更容易检测74=] 数学.


正如 评论的那样,所有人(尤其是初学者)最好避免混合使用 signedunsigned 并以其他方式编码需要时小心。

这里的一些答案提到了有符号和无符号值之间令人惊讶的提升规则,但这似乎更像是与 混合 有符号和无符号值有关的问题,但事实并非如此必须解释为什么 signed 变量在混合场景之外优于 unsigned

根据我的经验,除了混合比较和提升规则之外,无符号值吸引漏洞的主要原因有以下两个。

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

无符号和有符号整数在它们的最小值和最大值处都有 不连续性,它们环绕(无符号)或导致未定义的行为(有符号)。对于 unsigned,这些点位于 UINT_MAX。对于 int,它们位于 INT_MININT_MAXINT_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 位值的溢出在物理上可能是不可能的)。即使可能,与“零”错误相比,溢出相关错误的可能性通常是微不足道的,并且无符号值也会发生溢出。因此 unsigned 结合了两个世界中最糟糕的情况:可能会溢出非常大的数值,并且在零处出现不连续性。签名只有前者。

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

即使在 unsigned 对您有一点好处的情况下:它对您的好处不大:如果您必须支持超过 20 亿个“东西”,您可能很快就必须支持超过 40 亿个。

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

在数学上,无符号值(非负整数)是有符号整数(简称为 _integers)的子集。2。然而 signed 值自然会从仅对 unsigned 值的运算中弹出,例如减法。我们可能会说无符号值在减法下不是 closed。有符号值并非如此。

想找到文件中两个未签名索引之间的“增量”?你最好按正确的顺序做减法,否则你会得到错误的答案。当然,您通常需要进行运行时检查以确定正确的顺序!将无符号值作为数字处理时,您经常会发现(逻辑上)有符号值无论如何都会不断出现,因此您不妨从有符号开始。

对位

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

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

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

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

在专门领域之外,64 位值在很大程度上消除了这种担忧。带符号的 64 位值的上限为 9,223,372,036,854,775,807 - 超过九 quintillion。这是很多纳秒(大约 292 年的价值)和很多钱。它也是一个比任何计算机都大的阵列,可能在很长一段时间内将 RAM 放在一个一致的地址 space 中。所以也许 9 quintillion 对每个人来说都足够了(现在)?

何时使用无符号值

请注意,风格指南并不禁止甚至不一定不鼓励使用无符号数字。结尾是:

Do not use an unsigned type merely to assert that a variable is non-negative.

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

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

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

  • 当你真正想要模运算的时候。有时您实际上需要 2^N 模运算。在这些情况下,“溢出”是一个特性,而不是一个错误。无符号值在这里给你你想要的,因为它们被定义为使用模块化算术。有符号值根本不能(轻松、有效地)使用,因为它们具有未指定的表示形式并且溢出是未定义的。


0.5 在我写完这篇文章后,我意识到这几乎与我之前从未见过的 相同 - 有充分的理由,这是一个很好的例子!

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

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

我对 Google 的风格指南有一些经验,也就是很久很久以前进入公司的糟糕程序员的疯狂指令漫游指南。这条特别的指导方针只是那本书中许多疯狂规则的一个例子。

如果您尝试对无符号类型进行算术运算(请参阅上面的 Chris Uzdavinis 示例),换句话说,如果您将它们用作数字,则错误只会发生在无符号类型上。无符号类型不是用来存储数字数量的,它们是用来存储 counts 的,比如容器的大小,它永远不能是负数,它们可以而且应该用于这个目的。

使用算术类型(如有符号整数)来存储容器大小的想法是愚蠢的。你也会使用双精度数来存储列表的大小吗? Google 的人使用算术类型存储容器大小,并要求其他人做同样的事情,这说明了这家公司的一些情况。关于这些规定,我注意到的一件事是,他们越笨,他们就越需要严格遵守“要么做,要么你被解雇”的规则,否则有常识的人会忽视这条规则。

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

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

Google编码指南强调第一种考虑。其他准则集,例如 C++ Core Guidelines, put more emphasis on the second point. For example, consider Core Guideline I.12:

I.12: Declare a pointer that must not be null as not_null

Reason

To help avoid dereferencing nullptr errors. To improve performance by avoiding redundant checks for nullptr.

Example

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

By stating the intent in source, implementers and tools can provide better diagnostics, such as finding some classes of errors through static analysis, and perform optimizations, such as removing branches and null tests.

当然,您可以为整数争取一个 non_negative 包装器,它可以避免这两类错误,但是这会有其自身的问题...

google 语句是关于使用无符号作为容器 的大小类型。相比之下,这个问题似乎更笼统。继续阅读时请牢记这一点。

由于到目前为止大多数答案都对 google 声明做出了反应,但对更大的问题反应较少,我将从负容器尺寸开始我的回答,然后尝试说服任何人(绝望,我知道...... ) unsigned 是好的。

已签名的容器尺寸

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

现在,class 一群人喜欢谈论数学以及在这种情况下什么是“自然”。具有负数的整数类型如何自然地描述本质上 >= 0 的东西?经常使用负数数组?恕我直言,尤其是有数学倾向的人会发现这种语义不匹配(size/index 类型说负数是可能的,而负数大小的数组很难想象)令人恼火。

因此,关于此问题的唯一问题是——如 google 评论中所述——编译器是否真的可以积极协助查找此类错误。甚至比替代方案更好,后者是下溢保护的无符号整数(x86-64 程序集和可能其他体系结构有实现这一目标的方法,只有 C/C++ 不使用这些方法)。我能理解的唯一方法是,如果编译器自动添加 运行 时间检查(if (index < 0) throwOrWhatever),或者在编译时操作产生大量潜在误报的情况下 warnings/errors“这个索引阵列访问可能是负面的。”我有疑问,这会有所帮助。

此外,实际编写 运行 的人会检查他们的 array/container 索引,更多 处理有符号整数。您现在必须写成:if (index >= 0 && index < container.size()) { ... },而不是 if (index < container.size()) { ... }。在我看来像是强迫劳动而不是进步...

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

是的,这是对 java 的攻击。现在,我来自嵌入式编程背景,我们在现场总线方面进行了大量工作,其中二进制运算(和、或、异或、...)和值的按位组合实际上是面包和黄油。对于我们的一个产品,我们——或者更确切地说是一个客户——想要一个 java 端口……我坐在对面,幸运的是,他做端口的人非常能干(我拒绝了……)。他试图保持镇定......并默默忍受......但痛苦就在那里,经过几天不断处理带符号的整数值后,他无法停止诅咒,这些值应该是无符号的......甚至为编写单元测试这些场景是痛苦的,我个人认为 java 如果他们省略带符号的整数并只提供无符号的整数会更好......至少那样,你不必关心符号扩展等......你仍然可以将数字解释为 2 的补码。

这是我对此事的 5 美分。