为什么size_t是无符号的?

70

《C++程序设计语言》中,Bjarne Stroustrup写道:

unsigned整数类型非常适合将存储视为位数组的用途。为了表示正整数而使用unsigned而不是int来获得多一个比特位几乎从不是一个好主意。通过声明变量为unsigned来确保某些值为正数的尝试通常会被隐式转换规则所破坏。

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

另一篇相关文章是Scott Meyers的接口中的有符号和无符号类型。总结起来,他建议在接口中不使用无符号整数,无论值是否始终为正数。换句话说,即使负值没有意义,你也不应该必然使用无符号整数。


14
让它不签名会是一个“错误”吗? - Nicol Bolas
6
因为这是一个在接口中使用的无符号类型,而Meyers不建议使用,Stroustrup似乎在上面的引用中也表示这不是一个好主意。 - Jon
2
请注意,Stroustrup并没有创造C语言。在早期,空间/性能优化非常重要,否则大多数人永远不会停止使用汇编语言编程。 - dbrank0
6
来自Herb Sutter的相关引用 https://youtu.be/Puio5dly9N8?t=2660: "使用int,除非你需要不同的东西,然后仍然使用有符号的东西,直到你真正需要不同的东西,然后才转向无符号。是的,在STL和标准库中使用无符号索引是一个错误。" - Jon
1
我认为梅耶斯在那篇文章中自相矛盾。他写道:“设计良好的类易于正确使用,难以错误使用”。但是,如果一个函数接受一个有符号整数,该整数只能接受正值,那么该函数很容易被错误使用,因为它的参数类型告诉用户有符号(因此负数)的值是可以接受的。但实际上不是这样的——它们将无条件地导致不希望发生的行为。因此,该函数很容易被错误使用。 - codesniffer
显示剩余4条评论
4个回答

69

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

在一个带有16位指针的架构中,比如“小型”模式的DOS编程,将字符串限制为32 KB是不切实际的。

因此,C标准通过所需的范围要求ptrdiff_t,即size_t的有符号对应类型和指针差异的结果类型,有效地为17位。

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

然而,在现代的32位或64位编程中,它们并不适用,更重要的考虑是 C 和 C++的不幸的隐式转换规则使得无符号类型变成了Bug的引发者,当它们用于数字(因此,算术运算和大小比较)时。以20/20的远见,我们现在可以看到,采用那些特定的转换规则,例如string( "Hi" ).length() < -3 实际上是完全不可行的,相当愚蠢的。但是,这个决定意味着在现代编程中,采用无符号类型来表示数字具有严重的劣势和没有优势 - 除了满足那些认为unsigned是一种自描述类型名称,而没有想到typedef int MyType的感觉。

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


4
我不同意“引虫器”的部分。C(++)不是那种应该随意编写代码的语言,在阅读和理解好的详细语言书籍或语言标准之前,不能先做出任何假设。我认为无知并不是怪罪某种语言特性的有效借口。该特性存在,如果使用该语言,则必须处理它,不管用户是否愿意。 C(++)和其他编程语言还有更多缺陷,例如浮点数。许多人开始使用它时带有各种在正常数学中才有效的假设。浮点数是一个错误吗? - Alexey Frunze
8
@Alex:我理解你的感受。然而,在C++中,我们之所以进行强类型检查(尽可能保持与C兼容),是因为人类是会犯错的。甚至有一个非常著名的术语用于描述当你只是让某些事情成为可能时出现问题的情况。 - Cheers and hth. - Alf
14
所有优秀的编译器都会对 string( "Hi" ).length() < -3 进行警告,但不会对两个有符号整数的比较发出警告;如果 size_t 被定义为有符号整数,你的生活不会变得更轻松,你只会犯不同类型的错误。 - Lie Ryan
3
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - phuclv
6
在32位系统上,这也是一个很大的问题。当你可以使用4GB地址时,你不想被限制在2GB的size_t大小上。 - rustyx
显示剩余10条评论

23

size_t是无符号的,因为负数大小没有意义。

(来自评论区:)

它并不是在确保什么,而是在陈述事实。你最后一次看到大小为-1的列表是什么时候?如果跟随这种逻辑,您会发现无符号应该根本不存在,位运算也不应被允许。- geekosaur

更重要的是,出于你应该思考的原因,地址不是带符号的。大小是通过比较地址生成的;将地址视为带符号的将会做错很多事情,并且对结果使用带符号值将以您对Stroustrup引用的阅读认为可以接受但实际上是不能被接受的方式丢失数据。也许您可以解释一下负地址应该做什么。- geekosaur


9
当 Stroustrup 写道“试图通过声明变量为无符号来确保某些值为正数”时,这不正是他所关注的问题吗? - Jon
12
Stroustrup(和Meyer)的观点是,仅仅因为一个值永远不可能是负数,并不意味着你应该将其设置为无符号类型。首先,你无法再检测到传递给接口的错误负数值(它们会被隐式转换)。 - Jon
5
这应该是你的回答(size_t 存在是为了比较地址),而不是“负大小没有意义”? 后者似乎与 Stroustrup 和 Meyers 所说的相矛盾。 - Jon
7
@Jon:这个警告提示你存在运行时错误的可能性并应该修复。同样,如果你修复它(通过使函数采用有符号整数或确保不传递负值),那么就没有问题了。如果你不修复它,只是进行类型转换以使编译器停止报错,那么你会自食其果。 - Nicol Bolas
9
我的编译器在这里没有给出任何警告:size_t x = 0; for(size_t i=10; i>=x; --i) {} - 你的呢? - Benjamin Lindley
显示剩余13条评论

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位嵌入式处理器或建立一个抽象容器类型用于稀疏数据覆盖大范围时,它可能是一个非常真实的问题,这仍然可以提供与本机容器相同的API。

1

另一方面...

迷思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的字符串,但您可以轻松地拥有一个超过2G位的位集。而且不要忘记为稀疏数据设计的虚拟容器。

迷思3:您总是可以使用更宽的有符号类型。

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

但是如果您正在构建一个大型索引表,怎么办?当您只需要一个位时,您真的想要每个索引额外两个或四个字节吗?即使您有足够的内存和现代处理器,使该表扩大一倍可能会对参考位置产生不良影响,并且所有范围检查现在都是两步,降低了分支预测的有效性。而如果您没有那么多内存呢?

神话4:无符号算术是令人惊讶和不自然的。

这意味着有符号算术并不令人惊讶或者更自然。也许在数学方面思考时,所有基本算术运算都在所有整数集合上关闭。

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

这是一个错误:

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

如果min和max是有符号的,相加可能会溢出,从而导致未定义行为。我们大多数人经常忽略这种错误,因为我们忘记了加法不适用于有符号整数集合。我们之所以能够继续使用它,是因为我们的编译器通常会生成一些合理的代码(但仍然令人惊讶)。
如果min和max是无符号的,相加仍然可能溢出,但是未定义行为消失了。你仍然会得到错误的答案,所以仍然令人惊讶,但不会比有符号整数更令人惊讶。
真正令人惊讶的无符号操作是减法:如果你从一个较小的无符号整数中减去一个较大的无符号整数,你将得到一个很大的数字。这个结果与除以0一样令人惊讶。
即使你能够在所有的API中消除无符号类型,如果你处理标准容器、文件格式或网络协议,你仍然必须准备好应对这些无符号的“惊喜”。值得为解决问题的一部分而给你的API增加摩擦吗?

3
“而且不要忘记为稀疏数据设计的虚拟容器。”这样的容器将使用足够大的大小/索引类型来存储它们可以存储的数据。在32位系统上,它们仍应使用64位整数。就像文件API早已停止使用int作为文件大小一样。即使是C++17的文件系统API也不依赖于size_t用于文件大小;它使用uintmax_t。因此,这仍然不是size_t无符号的合理原因。 - Nicol Bolas
2
“你真的想为每个索引额外增加两个或四个字节吗?当你只需要一个比特时?” 我怎么知道我只需要一个比特?如果我确实知道我的索引永远不会超过某个大小,那么我可以使用适当的类型。但是,如果我有一个需要存储该表中可能出现的任何索引的表,则它需要能够存储任何索引。 过早的优化是不明智的。 - Nicol Bolas
@Nicol Bolas:虚拟容器示例是专门为反驳从不使用无符号类型的人常常提出的特定论点而设计的:即你永远不会有一个索引覆盖一半内存的容器。 - Adrian McCarthy
2
但这不是争论的重点。争论的是,你永远不会有一个容器,其索引覆盖了一半的内存空间,而不知道你正在编写这样的容器。它永远不会是vectordeque或其他任何东西;它总是一个明确设计为巨大的特定数据结构。因此,你将使用适合于容器预期大小的索引类型。 - Nicol Bolas
1
它不会是一个向量或双端队列,但它可能希望提供兼容的API。 - Adrian McCarthy
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接