【问题标题】:Why prefer signed over unsigned in C++? [closed]为什么更喜欢在 C++ 中签名而不是无符号? [关闭]
【发布时间】:2013-09-18 16:20:18
【问题描述】:

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

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

我之所以问这个问题是因为 Bjarne Stroustrup 和 Chandler Carruth 都给出了更喜欢 int 而不是 unsigned here (approx 12:30') 的建议。

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

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

【问题讨论】:

  • 为了现在无法观看 1 小时视频的读者的利益:Stroustrup 和 Carruth 对他们为什么更喜欢签名有何看法?
  • 我更喜欢int 而不是unsigned,因为:1. 它更短(我是认真的!),2. 它更通用、更直观(即我喜欢能够假设1 - 2 是-1,而不是一些不起眼的巨大数字),3. 如果我想通过返回超出范围的值来表示错误怎么办?
  • 11:08:“没有简单的指导可以给出”
  • @john 好的,但是他们至少需要使int溢出定义的行为并强制使用二进制补码。否则,int 将无法用于许多合理的任务,而 unsigned ints 则无法解决,就像现在一样。
  • @john:在大多数情况下,首选int 而不是unsigned 是一回事;从语言中删除unsigned 更加激烈。

标签: c++ optimization


【解决方案1】:

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

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

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

如果一种语言为数字和代数环数提供了有符号和无符号表示形式的选择,那么使用无符号数来表示始终为正的量可能是有意义的。但是,如果唯一的无符号类型表示代数环的成员,并且唯一表示数字的类型是有符号的,那么即使一个值总是正数,它也应该使用一种旨在表示数字的类型来表示。

顺便说一句,(uint32_t)-1 为 0xFFFFFFFF 的原因是,将有符号值转换为无符号值等同于将无符号值加零,而将整数与无符号值相加定义为将其大小加或减为/根据代数环规则的无符号值,该规则指定如果 X=YZ,则 X 是该环的唯一成员,例如 X+Z=Y。在无符号数学中,0xFFFFFFFF 是唯一一个在添加到无符号 1 时会产生无符号零的数字。

【讨论】:

  • 吹毛求疵:字段允许除加性标识之外的任何内容。如果你只有+-*,那么代数结构就是一个
  • @ChrisWhite:谢谢。上面更正了。自从我学习抽象代数以来已经很久了。我最初说的是“组”,但组不支持乘法。
  • @Chris:但无符号整数类型确实可以除加性标识之外的任何东西——它只是基于带有舍入的自然算术,而不是模块化等价类。
  • @BenVoigt 当然,当然。但是那个“除法”不是乘法的逆,因此不会使集合成为一个字段。但这都是语义,我想我们都知道我们在说什么:)
  • @Chris:你能想象如果 C++ 在它的一种原始类型上确实有伽罗瓦域划分会导致混乱吗?
【解决方案2】:

没有理由更喜欢signed 而不是unsigned,除了纯粹的社会学原因,即有些人认为普通程序员没有足够的能力和/或注意力来编写unsigned 类型的适当代码。这通常是各种“演讲者”使用的主要理由,无论这些演讲者有多么受人尊重。

实际上,称职的程序员可以快速开发和/或学习一组基本的编程习惯用法和技能,从而使他们能够根据无符号整数类型编写适当的代码。

还要注意,在 C 和 C++ 语言的其他部分(如指针算术和迭代算术)中,有符号和无符号语义之间的基本区别总是存在(以表面上不同的形式)。这意味着在一般情况下,程序员实际上并没有选择避免处理特定于无符号语义的问题以及它带来的“问题”。 IE。无论您是否愿意,您都必须学习使用在其左端突然终止并在此处终止的范围(而不是在远处的某个地方),即使您坚决避免使用unsigned 整数。

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

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

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

在我的实践中,我尽我所能坚持使用unsigned,仅在确实需要时使用signed

【讨论】:

  • 我不同意。这不是关于能力,而是关于共同点。 (比如何时使用类 vs 结构)有很多称职的程序员可以完美地告诉您他们何时可以使用无符号或有符号值,但出于这些“社会学”原因,无论如何都要使用有符号。 (我认为甚至缩进也用于此目的 - 是的,目的是使代码更易于阅读,但这也是 int 的重点)。
  • 我倾向于同意评论,因为每当变量的值将是无符号的时,我都会使用无符号,就像在只有正值的循环中一样 for (unsigned int i=0; i < 5; ++i) 我觉得它有点一个额外的类型说明符,但我也看到你的观点,只有 int 本身就使代码更简洁。
  • @Michael:不是这样。这是那些“听起来正确”的虚假智慧之一。例如,人们用来证明“尤达比较”语法的一种。例如。他们说应该写3 == xx == 3 以避免意外使用赋值而不是==。但实际上,这是一个永远不会发生的假问题。使用普通语法x == 3 的人根本不会犯这个错误。与unsigned 相同。一个称职的开发人员永远不会编写像i - 3 < 0 这样的代码,因为表达它的自然方式是i < 3,并且它是“与签名无关的”。
  • @AnT:与零相减和比较可能不是特别有用,但与另一个数字相减和比较可能有用。我认为(uint32_t)(x-y) < z 是一种合理的方式来检查y 是否在x 的一定距离内但不在以下范围内,但如果它可以习惯性地编写而不必命名特定类型会更好(虽然0u+x-y < z 应该适用于 xy 是相同无符号类型的所有情况,无论它是否大于或小于 int,我不认为 0u+ 被认为是公认的成语。
【解决方案3】:

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

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

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

【讨论】:

  • +1 表示“在出现问题时更明显”
  • 我来自汇编程序背景 - CPU 状态标志中总是编码溢出。最好能简单地访问这些信息,而不是为了注意到这些信息而需要从你的范围中切出一点,不是吗?
  • 从技术上讲,c++ 不必在 cpu 上运行,更不用说带有提供此类信息的标志的 CPU。溢出是未定义的行为,因此您“应该”确保它们不会首先发生。但是,是的,那会很好:P
  • “如果它是有符号整数,则变量将小于零并且明显错误” - 此溢出属性在 Java 和 C# 等语言中是正确的,但在 C/C++ 中不保证。在 C 和 C++ 中,溢出带符号的 int 是未定义的行为 - 因此您的程序可以显示正值或做一些完全出乎意料的事情。注意不要依赖溢出的整数作为检查完整性的方法。
【解决方案4】:

几个原因:

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

  2. unsigned 具有污染算术的讨厌特性,它与有符号整数混合。所以,如果你把有符号和无符号相加并询问结果是否大于零,你可能会被咬,尤其是当无符号整数类型隐藏在typedef 后面时。

【讨论】:

  • #2 咬过我一次。啊啊!
  • signed1 - signed2 也不安全,因为如果它溢出,你会得到未定义的行为。
  • 我认为这些才是真正的原因。
  • 除非signed1 和/或signed2 为“大”(超过最大可表示值的一半),否则签名大小写不会溢出。相比之下,从无符号值中减去 anything 会导致其回绕。
  • @Mordachai:不创建此异常的原因有很多。它会阻止编译器进行许多有用的优化。请参阅blog.regehr.org/archives/213 进行讨论。
【解决方案5】:

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

安德烈亚历山德雷斯库

  • 没有简单的指导方针。
  • 在系统编程中,我们需要不同大小和符号的整数。
  • 许多转换和神秘规则支配着算术(例如 auto),因此我们需要小心。

钱德勒·卡鲁斯

  • 这里有一些简单的指导方针:
    1. 除非您需要二进制补码算法或位模式,否则请使用有符号整数
    2. 使用足够的最小整数。
    3. 否则,如果您认为可以计算项目,请使用 int,如果超过您想要计算的数量,请使用 64 位整数。
  • 不用担心,当您需要不同的类型或尺寸时,请使用工具告诉您。

Bjarne Stroustrup

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

除了对签名规则的谨慎之外,我的一句话从专家那里得到了:

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

【讨论】:

  • 我觉得这个答案很有趣;但是,您能否详细说明“如果您认为可以计算项目,请使用int”?特别是,当我们必须与size_t 变量进行比较时,这是否与“从不将有符号与无符号混合”的规则相冲突?
  • 他只是在我的 OP 的视频中引用演讲者给出的答案。他们确实第二次回来并谈到这个话题,包括 Herb Sutter 说在 size_t 的情况下,标准库“弄错了……对不起。”
  • @Alberto 关于“使用int,如果你认为你可以计算项目”,我们实际上是在将它与使用像(签名)long 这样的类型进行对比,这不会挑战“永远不要将有符号与无符号混用”的规则。
【解决方案6】:

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

【讨论】:

    【解决方案7】:

    在做简单的算术运算时会给出意想不到的结果:

    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
    

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

    【讨论】:

    • 然而,与标准完美定义的任何其他规则相比,这些并不是更多的“故障”。我认为有符号溢出的未定义行为更像是一个“故障”。确实,无符号行为可能有点违反直觉,但 "malfunction" 在这里绝对是错误的词。
    • @ChristianRau 改写
    • 有趣。我发现您的示例非常合理,并且可以预期。在处理不能为负数的事物时,无符号(环)算术对我来说似乎比整数更明智。如果这真的是使用无符号的可怕部分,那么我很满意该建议只是通用目的,并且主要基于约定而不是任何严重的问题(当问题域不需要负值时)。
    【解决方案8】:
    1. 默认使用int:它与语言的其余部分配合得更好

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

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

      • 其规则是 complicated 并且令人惊讶(其中一个可以转换为另一个,具体取决于相对类型大小)
      • 开启-Wconversion -Wsign-conversion -Wsign-promo(这里gcc比Clang好)
      • 标准库错误地使用了 std::size_t(引自 GN13 视频)
      • 如果可以,请使用 range-for,
      • for(auto i = 0; i &lt; static_cast&lt;int&gt;(v.size()); ++i) 如果必须的话
    4. 不要使用短类型或大类型,除非你确实需要它们

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

    【讨论】:

    • 最佳时间性能通常取决于您可以在缓存中容纳多少数据......然后小类型轻松击败 32 位。
    • “位移有符号类型是未定义的行为” - 不,它不是,但它可以是。
    • @ChristianRau 感谢您指出这一点,已更新。我不想引用 5.8/2 的全部内容,但这太捷径了。
    • @TemplateRex 好吧,不幸的是,它仍然不是未定义的行为,左移未定义,右移实现定义。如果您不想引用标准,最简单的方法可能就是说它可以是未定义的行为。不幸的是,做出准确的陈述伴随着完全正确的责任。 :-)
    • 您给出了一套指导方针,但几乎没有解释。你复杂的for 循环特别需要一些“解释”。 (我会说这是一个糟糕的指南——改用for (auto i = 0u; i &lt; v.size(); ++i)!——或者,甚至更好,for (auto i : indices(x))。)
    【解决方案9】:

    int 类型比 unsigned 类型更类似于数学整数的行为。

    仅仅因为某种情况不需要表示负值而更喜欢unsigned 类型是幼稚的。

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

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

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

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

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

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

    【讨论】:

    • 你肯定知道模算术是完全数学的,对吧?
    • @GManNickG 这就是为什么我说“数学整数”而不是“数学”。在许多常见情况下,模运算是不合适的。
    • 虽然for (i = max - 1; i &gt;= 0; i--) 不会终止,但请注意for (i = max - 1; i != -1; i--) 将按预期工作(并且独立于类型的签名)。
    • @Kaz:你的意思可能是自然数
    • @BenVoigt 为什么我要调用自然数 {1, 2, 3, ...};它们在这里几乎不相关,作为一种类型,它们有缺点,比如在减法下不闭合,在这方面它们比模同余更糟糕。
    【解决方案10】:

    回答实际问题:对于很多事情,这并不重要。 int 可以更容易地处理第二个操作数比第一个大的减法之类的事情,你仍然会得到“预期”的结果。

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

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

    (我认为还有一两种其他情况,但结果是一样的——签名或未签名并不重要,两者执行操作的努力是相同的)。

    【讨论】:

    • 正确,高度相关....并且不回答问题。仍然有用,所以 +1
    【解决方案11】:

    int 是首选,因为它是最常用的。 unsigned 通常与位操作相关联。每当我看到unsigned 时,我都认为它是用来玩弄的。

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

    如果您使用索引对内容进行迭代,类型通常具有size_type,您不必关心它是有符号还是无符号。

    速度不是问题。

    【讨论】:

    • @ott-- 我不关注。 “设置标志”是什么意思?你是说你为无符号设置少一点?比如……你只写 31 位?
    • @ott:有很多(可能是大多数)值永远不会是负数。所以你的标志和设置它是不需要的。
    • @ott-- 你有参考吗?我仍然看不到使用无符号如何保存设置的标志或位置。
    • @ott--:在大多数现代处理器上,有符号和无符号加法的指令不是大致相同吗?此外,CPU 的速度不是取决于它需要做多少事情,而是取决于延迟(周期数)和时钟(因此实际上是关键路径的长度)[省略了诸如 OOO 执行或超标量架构之类的细节]。因此,只要它不增加关键路径,它就不会对速度产生任何影响,并且可以忽略功耗。
    • @ott:如果您谈论的是由 ALU 设置的 CPU 标志,您应该知道在许多架构上这些标志都设置为有符号和无符号。 CPU 没有太多数据类型的概念。
    【解决方案12】:

    现代架构的速度是一样的。 unsigned int 的问题在于它有时会产生意想不到的行为。这可能会产生在其他情况下不会出现的错误。

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

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

    确实,如果你正确地保护你的代码,你就不会遇到这个问题,如果你在任何地方都虔诚地使用 unsigned,你会没事的(前提是你只是加法,而不是减法,并且你永远不会接近 MAX_INT )。我到处使用 unsigned int 。但这需要很多纪律。对于很多程序,您可以使用int 来解决问题,并将时间花在其他错误上。

    【讨论】:

    • unsigned int 的问题在于它有时(在溢出的情况下)会产生意外行为。” signed int 的问题在于它有时(在溢出的情况下)会产生未定义的行为。鉴于这些选择,unsigned 看起来很不错;)
    • (当然,完全不同的值会发生溢出,所以溢出对于有符号类型来说很少出现问题)
    • @BenVoigt 另外,“意外”只有在不知道隐式转换规则的情况下才会出现(这就是我所说的“反直觉”)。幸运的是,无符号溢出是 100% 由 C 和 C++ 标准精确定义的(好吧,据我所知)。
    • @H2CO3:除了超出范围的移位操作数,这是我所知道的无符号类型的唯一 UB 示例。
    • @BenVoigt 啊是的,很好的轮班。
    【解决方案13】:

    根据 cmets 中的请求:我更喜欢 int 而不是 unsigned 因为...

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

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

    3. 如果我想通过返回超出范围的值来表示错误怎么办?

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

    【讨论】:

    • 我觉得很遗憾说它更短需要(我是认真的)。
    • @BobbyDigital 确实如此。应该少担心“效率”,多关注正确性、可读性和整体风格。
    • @H2CO3:如果int 的正值范围足以满足您的目的,那么它们UINT_MAX 是一个非常好的超出范围值,用于指示错误情况。事实上,-1 可以在代码中用于此目的,因为当转换为 unsigned 时,它的计算结果为 UINT_MAX
    • 键入“无符号”并不完全是腕管诱导 ;)
    • @Mordachai 这不是关于可写性,而是关于可读性。
    猜你喜欢
    • 2020-07-21
    • 2012-10-27
    • 1970-01-01
    • 1970-01-01
    • 2015-08-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-24
    相关资源
    最近更新 更多