【问题标题】:Why is size_t unsigned?为什么 size_t 是无符号的?
【发布时间】:2012-04-27 10:13:31
【问题描述】:

Bjarne Stroustrup 在 C++ 编程语言中写道:

无符号整数类型非常适合将存储视为 位数组。使用无符号而不是 int 来获得更多位 表示正整数几乎从来都不是一个好主意。尝试去 通过声明变量无符号来确保某些值是正数 通常会被隐式转换规则打败。

size_t 似乎是无符号的“以获得更多位来表示正整数”。那么这是一个错误(或权衡)吗?如果是,我们是否应该尽量减少在我们自己的代码中使用它?

Scott Meyers 的另一篇相关文章是here。总而言之,他建议不要使用无符号接口,无论该值是否始终为正。换句话说,即使负值没有意义,也不一定要使用无符号。

【问题讨论】:

  • 为什么不签名会是一个“错误”?
  • @Nicol:因为它是一个在接口中使用的无符号,Meyers 建议不要这样做,而且 Stroustrup 似乎在上面的引用中说这不是一个好主意。
  • 阿尔夫的回答看起来可能是正确的。人们倾向于使用 size_t 既是标准的又是无符号的这一事实,因此他们应该在自己的代码中使用 size_t 或无符号类型。如果答案是“size_t 由于历史原因未签名”之类的,那么这会稍微减少这种理由。
  • 请注意,Stroustrup 没有创建 C。在早期,空间/性能优化非常重要,否则大多数人永远不会停止在汇编中编码。
  • Herb Sutter youtu.be/Puio5dly9N8?t=2660 的相关引用:“使用 int 除非你需要不同的东西,然后仍然使用有符号的东西,直到你真的需要不同的东西,然后诉诸无符号的。是的,不幸的是我们在 STL 和标准库中使用无符号索引的错误。”

标签: c++ unsigned-integer size-t


【解决方案1】:

size_t 由于历史原因未签名。

在具有 16 位指针的体系结构上,例如“小型”模型 DOS 编程,将字符串限制为 32 KB 是不切实际的。

因此,C 标准要求(通过要求的范围)ptrdiff_tsize_t 的签名对应物和指针差异的结果类型实际上是 17 位。

这些原因仍然适用于嵌入式编程领域的某些部分。

但是,它们不适用于现代 32 位或 64 位编程,其中更重要的考虑是 C 和 C++ 不幸的隐式转换规则使无符号类型成为错误吸引器,当它们用于数字(因此,算术运算和幅度比较)。事后看来,我们现在可以看到 20-20 的决定采用那些特定的转换规则,例如string( "Hi" ).length() < -3 实际上是有保证的,相当愚蠢和不切实际。然而,这个决定意味着在现代编程中,对数字采用无符号类型有严重的缺点,没有任何优点——除了满足那些认为 unsigned 是一个自我描述的类型名称而没有想到 @987654326 的人的感受@。

总结起来,这不是一个错误。出于当时非常合理、实用的编程原因,这是一个决定。它与将期望从像 Pascal 这样的边界检查语言转移到 C++ 没有任何关系(这是一种谬误,但非常普遍,即使其中一些人从未听说过 Pascal)。

【讨论】:

  • 我不同意“bug吸引器”部分。 C(++) 不是一种应该随便写的语言,在阅读和理解一本关于语言或语言标准本身的详细的好书之前做出假设。我不认为无知是指责语言功能的正当借口。它就在那里,如果他们使用它,无论他们是否愿意,都必须处理它。还有更多关于 C(++) 和其他编程语言的问题。以浮点数为例。许多人开始使用仅在普通数学中有效的各种假设。 FP错了?
  • @Alex:我理解你的感受。然而,我们在 C++ 中尽可能地保持 C 兼容性的强类型检查的原因是人类容易犯错。当你让事情成为可能时,甚至有一个非常知名的名字。
  • 所有优秀的编译器都会对string( "Hi" ).length() < -3 发出警告,但不会对两个有符号整数之间的比较发出警告;如果size_t 被定义为已签名,您的生活将不会变得更轻松,您只会犯不同类型的错误。
  • 这在 32 位系统上也是一个很大的问题。当您可以寻址高达 4GB 时,您不想被限制为 2GB size_t。
  • @RustyX:这不是问题。使用 32 位签名 ptrdiff_t 仅排除单个大于 2GB 的 char 数组。当有人指出这一点时,有人说他们经常使用如此大的(相对于地址空间)char 数组。我不相信他们。无论如何,大多数 32 位 Windows 程序都限制为 2GB。这在很长一段时间内运作良好。
【解决方案2】:

size_tunsigned,因为负尺寸没有意义。

(来自cmets:)

与其说是确保,不如说是说明是什么。您最后一次看到大小为 -1 的列表是什么时候?过于遵循这个逻辑,你会发现 unsigned 根本不应该存在,也不应该允许位操作。 – geekosaur

更重要的是:由于您应该考虑的原因,地址没有签名。大小是通过比较地址生成的;将地址视为已签名会做很多错误的事情,并且使用签名值作为结果会丢失数据,而您对 Stroustrup 引用的阅读显然认为可以接受,但实际上并非如此。也许您可以解释一下否定地址应该做什么。 – geekosaur

【讨论】:

  • 这不正是 Stroustrup 在写“尝试通过声明变量无符号来确保某些值是正数...”时所要解决的问题吗?
  • Stroustrup(和 Meyer)的观点是,仅仅因为一个值永远不会是负数,并不意味着你应该让它无符号。一方面,您无法再检测到接口中传递的错误负值(隐式转换)。
  • 不应该是你的答案(size_t 存在用于比较地址),而不是“负尺寸没有意义”?后者似乎与 Stroustrup 和 Meyers 所说的相矛盾。
  • @Jon:警告让您知道存在运行时错误的可能性并且应该修复。同样,如果您修复它(通过使函数采用带符号的 int,或确保不能传入负值),就没有问题。如果你不修复它,如果你只是做一个强制转换来关闭编译器,那么你应该得到你所得到的。
  • @NicolBolas:我的编译器在这里没有给出警告:size_t x = 0; for(size_t i=10; i>=x; --i) {} -- 你的呢?
【解决方案3】:

使索引类型无符号的一个原因是为了与 C 和 C++ 对半开区间的偏好对称。如果你的索引类型是无符号的,那么你的大小类型也可以是无符号的。


在 C 中,您可以有一个指向数组的指针。有效指针可以指向数组的任何元素或数组末尾之后的一个元素。它不能指向数组开头之前的一个元素。

int a[2] = { 0, 1 };
int * p = a;  // OK
++p;  // OK, points to the second element
++p;  // Still OK, but you cannot dereference this one.
++p;  // Nope, now you've gone too far.
p = a;
--p;  // oops!  not allowed

C++ 同意并将这个想法扩展到迭代器。

针对无符号索引类型的论证经常会抛出一个从后到前遍历数组的例子,代码通常是这样的:

// WARNING:  Possibly dangerous code.
int a[size] = ...;
for (index_type i = size - 1; i >= 0; --i) { ... }

此代码index_type 被签名时有效,它用作索引类型应该被签名的参数(并且,通过扩展,大小应该被签名)。

该论点没有说服力,因为该代码是非惯用的。观察如果我们尝试用指针而不是索引重写这个循环会发生什么:

// WARNING:  Bad code.
int a[size] = ...;
for (int * p = a + size - 1; p >= a; --p) { ... }

哎呀,现在我们有未定义的行为!忽略size为0时的问题,我们在迭代结束时遇到问题,因为我们生成了一个指向第一个元素之前的元素的无效指针。即使我们从未尝试取消引用该指针,这也是未定义的行为。

因此,您可以争辩通过更改语言标准来解决此问题,以使其合法地具有指向第一个元素之前的元素的指针,但这不太可能发生。半开区间是这些语言的基本组成部分,所以让我们改写更好的代码。

一个正确的基于指针的解决方案是:

int a[size] = ...;
for (int * p = a + size; p != a; ) {
  --p;
  ...
}

许多人发现这令人不安,因为减量现在在循环体中而不是在标头中,但是当您的 for 语法主要设计用于通过半开间隔的前向循环时,就会发生这种情况。 (反向迭代器通过推迟递减来解决这种不对称性。)

现在,以此类推,基于索引的解决方案变为:

int a[size] = ...;
for (index_type i = size; i != 0; ) {
  --i;
  ...
}

无论index_type 是有符号还是无符号,这都有效,但无符号选项会产生更直接映射到惯用指针和迭代器版本的代码。无符号还意味着,与指针和迭代器一样,我们将能够访问序列的每个元素——我们不会放弃一半的可能范围来表示无意义的值。虽然这在 64 位世界中不是一个实际问题,但在 16 位嵌入式处理器或为大范围的稀疏数据构建抽象容器类型时可能是一个非常现实的问题,仍然可以提供与原生容器。

【讨论】:

    【解决方案4】:

    另一方面...

    误区 1std::size_t 未签名是因为旧版限制不再适用。

    这里通常提到两个“历史”原因:

    1. sizeof返回std::size_t,自C时代以来一直未签名。
    2. 处理器的字长更小,因此挤出额外的范围很重要。

    但是,这些原因尽管已经很老了,但实际上都没有成为历史。

    sizeof 仍然返回一个std::size_t,它仍然是未签名的。如果您想与sizeof 或标准库容器互操作,则必须使用std::size_t

    替代方案都更糟:您可以禁用有符号/无符号比较警告和大小转换警告,并希望这些值始终在重叠范围内,这样您就可以忽略使用不同类型的潜在错误对可能引入。或者您可以进行大量范围检查和显式转换。或者您可以通过巧妙的内置转换引入自己的尺寸类型以集中范围检查,但没有其他库会使用您的尺寸类型。

    虽然大多数主流计算都是在 32 位和 64 位处理器上完成的,但即使在今天,C++ 仍然用于嵌入式系统中的 16 位微处理器。在这些微处理器上,拥有一个字大小的值来表示内存空间中的任何值通常非常有用。

    我们的新代码仍然需要与标准库进行互操作。如果我们的新代码使用有符号类型,而标准库继续使用无符号类型,我们会让每个必须同时使用这两种类型的消费者变得更加困难。

    误区 2:您不需要额外的部分。 (A.K.A.,当你的地址空间只有 4GB 时,你永远不会有大于 2GB 的字符串。)

    大小和索引不仅仅用于记忆。您的地址空间可能有限,但您可能会处理比地址空间大得多的文件。虽然您可能没有超过 2GB 的字符串,但您可以轻松地拥有超过 2GB 的位组。并且不要忘记为稀疏数据设计的虚拟容器。

    误区 3:您始终可以使用更广泛的有符号类型。

    并非总是如此。确实,对于一个或两个局部变量,您可以使用std::int64_t(假设您的系统有一个)或signed long long,并且可能编写完全合理的代码。 (但您仍然需要一些显式强制转换和两倍的边界检查,否则您将不得不禁用一些编译器警告,这些警告可能会提醒您代码中其他地方的错误。)

    但是,如果您要构建一个大型索引表怎么办?当您只需要一个 bit 时,您真的需要为每个索引增加两个或四个 bytes 吗?即使您有足够的内存和现代处理器,将表增大一倍也可能对引用的局部性产生有害影响,并且您的所有范围检查现在都是两步的,从而降低了分支预测的有效性。如果你没有有那么多记忆怎么办?

    误区 4:无符号算术令人惊讶且不自然。

    这意味着 signed 算术并不令人惊讶或更自然。而且,也许是在从数学角度思考时,所有基本算术运算都在所有整数的集合上闭合。

    但是我们的计算机不能处理整数。它们使用整数的无穷小部分。我们的有符号算术在所有整数的集合上不是封闭的。我们有上溢和下溢。对许多人来说,这太令人惊讶和不自然,他们大多只是忽略它。

    这是错误:

    auto mid = (min + max) / 2;  // BUGGY
    

    如果 minmax 已签名,则总和可能会溢出,从而产生未定义的行为。我们大多数人经常会错过这些类型的错误,因为我们忘记了加法不是封闭在有符号整数集上的。我们侥幸成功,因为我们的编译器通常会生成一些合理的代码(但仍然令人惊讶)。

    如果minmax 是无符号的,总和仍可能溢出,但未定义的行为消失了。你仍然会得到错误的答案,所以它仍然令人惊讶,但并不比有符号整数更令人惊讶。

    真正的 unsigned 惊喜来自于减法:如果你从一个较小的 unsigned int 中减去一个较大的 unsigned int,你最终会得到一个很大的数字。这个结果并不比除以 0 更令人惊讶。

    即使您可以从所有 API 中消除无符号类型,如果您处理标准容器或文件格式或有线协议,您仍然必须为这些无符号“惊喜”做好准备。是否真的值得在您的 API 中添加摩擦以仅“解决”部分问题?

    【讨论】:

    • "不要忘记为稀疏数据设计的虚拟容器。" 这样的容器将使用足够大的大小/索引类型来存储它们可以存储的数据。在 32 位系统上,它们仍应使用 64 位整数。就像文件 API 早就停止使用 int 来确定文件大小一样。甚至 C++17 的文件系统 API 也不依赖 size_t 来确定文件大小;它使用uintmax_t。所以这仍然不是 size_t 未签名的正当理由。
    • "当您只需要一个位时,您真的需要为每个索引增加两个或四个字节吗?" 我怎么知道我只需要一个位?如果我真的知道我的索引将永远大于某个大小,那么我可以使用适当的类型。但是如果我有一个表需要存储可以出现在该表中的任何索引,那么它需要能够存储任何索引。过早的优化还为时过早。
    • @Nicol Bolas:虚拟容器示例是专门用来反驳永不签名阵营经常提出的具体论点:您永远不会拥有索引覆盖一半内存的容器。跨度>
    • 你永远不会有一个索引覆盖一半内存的容器。”但这不是论点。论据是,如果不知道您正在编写这样的容器,您将永远不会拥有这样的容器。它永远不会是vectordeque 或其他;它总是一个特定的数据结构,被明确地设计成巨大的大小。因此,您将使用适合容器预期大小的索引类型。
    • 它不会是向量或双端队列,但它可能希望提供兼容的 API。
    猜你喜欢
    • 2016-02-08
    • 1970-01-01
    • 2010-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多