为什么size_t没有签名

Why is size_t unsigned?

本文关键字:size 为什么      更新时间:2023-10-16

Bjarne Stroustrup在The C++ Programming Language中写道:

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

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

斯科特·迈耶斯(Scott Meyers)的另一篇相关文章在这里。总而言之,他建议不要在接口中使用 unsigned ,无论该值是否始终为正数。换句话说,即使负值没有意义,也不一定要使用 unsigned。

由于历史原因,size_t是无符号的。

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

出于这个原因,C 标准要求(通过所需的范围)ptrdiff_tsize_t 的有符号对应物和指针差值的结果类型,实际上是 17 位。

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

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

总而言之,这不是一个错误。这是一个出于当时非常理性、实用的编程原因的决定。这与将期望从像 Pascal 这样的边界检查语言转移到 C++ 无关(这是一个谬误,但是一个非常非常普遍的谬误,即使一些这样做的人从未听说过 Pascal)。

size_t unsigned因为负大小没有意义。

(来自评论:)

与其说是

确保,不如说是说明是什么。您最后一次看到大小为 -1 的列表是什么时候?遵循该逻辑太远,您会发现根本不应该存在无符号,也不应该允许位操作。- 极龙

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

使索引类型无符号的一个原因是与 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-syntax 主要设计用于通过半开放间隔的前向循环时,就会发生这种情况。 (反向迭代器通过推迟递减来解决这种不对称性。

现在,通过类比,基于索引的解决方案变为:

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

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

另一方面

...

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

这里通常提到的两个"历史"原因:

  1. sizeof返回std::size_t,自C时代以来一直没有签名。
  2. 处理器的字数较小,因此挤出额外的范围非常重要。

这两个原因,尽管很古老,但实际上都没有被归入历史。

sizeof仍返回仍未签名的std::size_t。 如果要与sizeof或标准库容器进行互操作,则必须使用 std::size_t

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

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

我们的新代码仍然必须与标准库进行互操作。 如果我们的新代码使用有符号类型,而标准库继续使用无符号类型,那么对于必须同时使用两者的每个使用者来说,我们都会变得更加困难。

误区2:你不需要额外的一点。(又名,当您的地址空间只有 4GB 时,您永远不会有大于 2GB 的字符串。

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

误区 3:始终可以使用更宽的签名类型。

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

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

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

这意味着有符号算术并不奇怪,或者在某种程度上更自然。 而且,也许是在数学方面思考时,所有基本的算术运算都关闭在所有整数的集合上。

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

这是错误:

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

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

如果 minmax 未签名,则总和仍可能溢出,但未定义的行为已消失。 你仍然会得到错误的答案,所以这仍然令人惊讶,但并不比签名的整数更令人惊讶。

真正的无符号

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

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