为什么更喜欢在C++中签名而不是未签名

Why prefer signed over unsigned in C++?

本文关键字:更喜欢 C++ 为什么      更新时间:2023-10-16

我想更好地了解为什么选择int而不是unsigned

就个人而言,除非有正当理由,否则我从不喜欢有符号值。 例如,数组中的项目计数,字符串的长度,或内存块的大小等,因此这些东西通常不可能是负数。 这样的值没有可能的意义。 为什么更喜欢int,因为它在所有此类情况下都具有误导性?

我问这个问题是因为Bjarne Stroustrup和Chandler Carruth都建议在这里更喜欢int而不是unsigned(大约12:30')。

我可以看到使用 int 而不是 shortlong 的论点 - int 是目标机器架构的"最自然"数据宽度。

但是签了未署名一直让我很恼火。 在典型的现代 CPU 架构上,有符号值真的更快吗? 是什么让他们变得更好?

根据评论中的要求:我更喜欢int而不是unsigned,因为...

  1. 它更短(我是认真的!

  2. 它更通用,更直观(即我喜欢能够假设1 - 2是 -1 而不是一些晦涩的巨大数字)

  3. 如果我想通过返回超出范围的值来发出错误信号,该怎么办?

当然也有反驳,但这些是我喜欢将我的整数声明为 int 而不是 unsigned 的主要原因。当然,这并不总是正确的,在其他情况下,unsigned只是任务的更好工具,我只是专门回答"为什么有人更喜欢默认签名"的问题。

让我解释一下视频,正如专家简洁地说的那样。

安德烈·亚历山德雷斯库

  • 没有简单的指导方针。
  • 在系统编程中,我们需要不同大小和符号的整数。
  • 许多转换和晦涩的规则控制着算术(如auto),所以我们需要小心。

钱德勒·卡鲁斯

  • 以下是一些简单的准则:
    1. 使用有符号整数,除非你需要二进制的补码算法或位模式
    2. 使用就足够了的最小整数。
    3. 否则,如果您认为可以计算项目,请使用 int,如果它甚至比您想要计数的还要多,请使用 64 位整数。
  • 不用担心,使用工具告诉您何时需要不同的类型或尺寸。

Bjarne Stroustrup

  • 使用int,直到您有理由不这样做。
  • 仅对位模式使用无符号。
  • 切勿混合使用有符号和无符号

撇开对签名规则的警惕不谈,我从专家那里学到的一句话:

使用适当的类型,当您不知道时,请使用int直到您知道为止。

几个原因:

  1. unsigned上的算术总是产生无符号的,这在减去可以合理导致负结果的整数量时可能是一个问题——想想减去货币数量以产生余额,或数组索引以产生元素之间的距离。如果操作数是无符号的,你会得到一个完美定义但几乎可以肯定是毫无意义的结果,并且result < 0比较将永远是错误的(现代编译器会幸运地警告你)。

  2. unsigned有一个令人讨厌的特性,那就是污染算术,它与有符号整数混合在一起。因此,如果您添加有符号和无符号并询问结果是否大于零,您可能会被咬,尤其是当无符号整数类型隐藏在typedef后面时。

除了纯粹的社会学问题之外,没有理由更喜欢signed而不是unsigned,即有些人认为普通程序员没有足够的能力和/或注意力来编写unsigned类型的正确代码。这通常是各种"发言者"使用的主要推理,无论这些发言者多么受人尊敬。

实际上,有能力的程序员可以快速开发和/或学习基本的编程习语和技能,使他们能够根据无符号整数类型编写适当的代码。

另请注意,有符号语义和无符号语义之间的根本差异始终存在于 C 语言和C++语言的其他部分(以表面上不同的形式),例如指针算术和迭代器算术。这意味着在一般情况下,程序员实际上没有选择避免处理特定于无符号语义的问题以及随之而来的"问题"。即,无论您是否愿意,您都必须学会处理在左端突然终止并在此处终止(而不是远处某处)的范围,即使您坚决避免使用unsigned整数。

此外,您可能知道,标准库的许多部分已经非常依赖于unsigned整数类型。强制使用有符号算术,而不是学习使用无符号算法,只会导致灾难性的糟糕代码。

在某些情况下,首选signed的唯一真正原因是,在混合整数/浮点代码中,signed FPU 指令集通常直接支持整数格式,而根本不支持unsigned格式,这使得编译器生成额外的代码用于浮点值和unsigned值之间的转换。在此类代码中,signed类型可能性能更好。

但同时在纯整数代码中,unsigned类型可能比signed类型性能更好。例如,整数除法通常需要额外的更正代码才能满足语言规范的要求。只有在负操作数的情况下才需要更正,因此在未真正使用负操作数的情况下会浪费 CPU 周期。

在我的实践中,我尽我所能坚持unsigned,只有在我真的需要时才使用signed

C 和从它派生的许多语言中的积分类型有两个一般用例:表示数字,或表示抽象代数环的成员。 对于那些不熟悉抽象代数的人来说,环背后的主要概念是,将环的两个项目相加、减去或相乘应该产生该环的另一个项目——它不应该崩溃或产生环外的值。 在 32 位机器上,将无符号0x12345678添加到无符号0xFFFFFFFF不会"溢出"——它只是产生为整数环全等 mod 2^32 定义的结果0x12345677(因为将 0x12345678 添加到 0xFFFFFFFF 的算术结果,即 0x112345677,与0x12345677 mod 2^32 一致)。

从概念上讲,这两种目的(表示数字,或表示整数环全等 mod 2^n 的成员)都可以由有符号和无符号类型提供,并且许多操作对于这两种用例都是相同的,但存在一些差异。 除其他事项外,尝试将两个数字相加不应期望产生正确的算术总和以外的任何结果。 虽然是否应该要求一种语言来生成必要的代码以保证它不会(例如,将抛出异常)是有争议的,但有人可能会争辩说,对于使用整数类型来表示数字的代码,这种行为比产生算术不正确的值更可取,并且编译器不应该被禁止以这种方式行事。

C 标准的实现者决定使用有符号整数类型来表示数字,使用无符号类型来表示整数全等模 2^n 的代数环的成员。 相比之下,Java使用有符号整数来表示这些环的成员(尽管它们在某些上下文中的解释不同;例如,不同大小的有符号类型之间的转换与无符号类型之间的转换行为不同),Java既没有无符号整数,也没有任何原始整数类型在所有非特殊情况下都表现为数字。

如果一种语言为数字和代数环数字提供了有符号和无符号表示的选择,那么使用无符号数字来表示始终为正的数量可能是有意义的。 但是,如果唯一的无符号类型表示代数环的成员,而表示数字的唯一类型是有符号类型,那么即使值始终为正数,也应使用设计用于表示数字的类型来表示。

顺便说一下,uint32_t-1 0xFFFFFFFF的原因源于这样一个事实,即将有符号值转换为无符号值等效于添加无符号零,而将整数添加到无符号值定义为根据代数环的规则将其大小添加到/从无符号值中减去,该规则指定如果 X=Y-Z, 那么X是该环的唯一成员,例如X+Z=Y。 在无符号数学中,0xFFFFFFFF是唯一一个当添加到无符号 1 时产生无符号零的数字。

速度在

现代架构上是一样的。unsigned int的问题在于它有时会产生意外行为。这可能会产生否则不会显示的错误。

通常,当您从值中减去 1 时,该值会变小。现在,对于signedunsigned int变量,会有一段时间减去 1 会创建一个更大的值。unsigned intint 之间的主要区别在于,对于unsigned int,生成矛盾结果的值是常用值--- 0 ---而有符号的数字安全地远离正常操作。

就为错误值返回 -1 而言,现代思维---抛出异常比测试返回值更好。

的确,如果你正确地捍卫你的代码,你就不会有这个问题,如果你在任何地方都虔诚地使用未签名,你会没问题(前提是你只是加法,从不减法,并且你永远不会接近MAX_INT)。我到处都使用无符号的 int。但这需要很多纪律。对于很多程序,你可以使用int,把时间花在其他错误上。

  1. 默认使用int:它与语言的其余部分配合得更好

    • 最常见的领域用法是常规算术,而不是模算术
    • int main() {} // see an unsigned?
    • auto i = 0; // i is of type int
  2. 仅将unsigned用于模运算和位摆动(特别是移位)

    • 具有与常规算术不同的语义,请确保它是您想要的
    • 位移符号类型很微妙(请参阅@ChristianRau的评论)
    • 如果您需要在 2 位计算机上使用 32> Gb 矢量,请升级您的操作系统/硬件
  3. 切勿混合使用有符号和无符号算术

    • 规则复杂且令人惊讶(根据相对类型大小,任何一个都可以转换为另一个)
    • 打开-Wconversion -Wsign-conversion -Wsign-promo(GCC在这里比Clang更好)
    • 标准库在std::size_t上出错了(引自 GN13 视频)
    • 如果可以的话,请使用范围,
    • for(auto i = 0; i < static_cast<int>(v.size()); ++i)是否必须
  4. 不要使用短类型或大类型,除非你确实需要它们

    • 当前体系结构的数据流很好地迎合了 32 位非指针数据(但请注意 @BenVoigt 关于较小类型的缓存效果的评论)
    • charshort节省空间,但受到整体促销的影响
    • 你真的要算到所有int64_t吗?

回答实际问题:对于大量事物来说,这并不重要。 int处理第二个操作数比第一个操作数大之类的事情可能会容易一些,并且您仍然可以获得"预期"结果。

在 99.9% 的情况下绝对没有速度差异,因为有符号和无符号数字的唯一不同指令是:

  1. 使数字变长(用符号填充有符号或零表示无符号) - 两者都需要相同的努力。
  2. 比较 - 一个有符号的数字,处理器必须考虑任何一个数字是否为负数。但同样,与有符号或无符号数字进行比较的速度相同 - 它只是使用不同的指令代码说"设置了最高位的数字小于未设置最高位的数字"(本质上)。[从学究上讲,几乎总是使用比较结果的操作是不同的 - 最常见的情况是条件跳转或分支指令 - 但无论哪种方式,它都是相同的努力,只是输入的含义略有不同]。
  3. 乘以和除法。显然,如果是有符号乘法,则需要对结果进行符号转换,如果设置了其中一个输入的最高位,则无符号不应更改结果的符号。再一次,努力(就像我们关心的那样)是相同的。

(我认为还有其他一两种情况,但结果是相同的 - 它是签名还是未签名真的无关紧要,执行操作的努力对于两者是相同的)。

int 类型比 unsigned 类型更接近数学整数的行为。

仅仅因为情况不需要表示负值而选择unsigned类型是幼稚的。

问题是unsigned类型的不连续行为接近于零。任何尝试计算小负值的操作都会生成一些大的正值。(更糟糕的是:一个由实现定义的。

诸如此类的代数关系a < b意味着a - b < 0在无符号域中被破坏,即使对于像 a = 3b = 4 这样的小值也是如此。

如果i为无符号,则像 for (i = max - 1; i >= 0; i--) 这样的下降循环无法终止。

无符号的怪癖可能会导致问题,该问题将影响代码,无论该代码是否期望仅表示正数量。

无符号类型

的优点是,对于无符号类型,某些未在位级别可移植定义的操作对于无符号类型也是如此。无符号类型缺少符号位,因此通过符号位移动和屏蔽不是问题。无符号类型适用于位掩码,以及以独立于平台的方式实现精确算术的代码。无符号运算将模拟 2 的补码语义,即使在非 2 的补码机器上也是如此。编写多精度 (bignum) 库实际上需要使用无符号类型的数组,而不是有符号类型。

无符号类型也适用于数字行为类似于标识符而不是算术类型的情况。例如,IPv4 地址可以用 32 位无符号类型表示。您不会将 IPv4 地址加在一起。

首选

int,因为它是最常用的。 unsigned通常与位操作相关联。每当我看到一个unsigned,我就认为它是用来叽叽喳喳的。

如果需要更大的范围,请使用 64 位整数。

如果你使用索引迭代东西,类型通常有size_type,你不应该关心它是有符号的还是无符号的。

速度不是问题。

对我来说

,除了 32 位架构上的有符号和无符号整数集中包含的 0..+2,147,483,647 范围内的所有整数之外,我需要使用 -1(或更小)的可能性比需要使用 +2,147,483,648(或更大)的可能性更高。

我能想到的一个很好的理由是检测到溢出。

对于数组中的项目计数、字符串长度或内存块大小等用例,您可能会溢出无符号的 int,即使您查看变量,您也可能不会注意到差异。如果它是一个有符号的int,则变量将小于零并且显然是错误的。

当您想使用它时,您可以简单地检查变量是否为零。这样,您就不必像无符号整数那样在每次算术运算后检查溢出。

在执行简单的算术运算时,它会给出意想不到的结果:

unsigned int i;
i = 1 - 2;
//i is now 4294967295 on a 64bit machine

在进行简单比较时,它会给出意想不到的结果:

unsigned int j = 1;
std::cout << (j>-1) << std::endl;
//output 0 as false but 1 is greater than -1

这是因为在执行上述操作时,有符号的整数被转换为无符号,并且它溢出并变成一个非常大的数字。

相关文章: