64

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

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

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

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

4

4 回答 4

66

size_t由于历史原因未署名。

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

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

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

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

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

于 2012-04-16T04:00:20.463 回答
25

size_tunsigned因为负尺寸没有意义。

(来自评论:)

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

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

于 2012-04-16T02:32:01.350 回答
4

使索引类型无符号的一个原因是为了与 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 位嵌入式处理器或为大量范围内的稀疏数据构建抽象容器类型时可能是一个非常现实的问题,仍然可以提供与本机容器。

于 2018-03-20T20:28:52.570 回答
-1

另一方面 ...

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

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

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

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

sizeof仍然返回一个std::size_t仍然未签名的。如果你想与sizeof标准库容器互操作,你将不得不使用std::size_t.

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

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

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

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

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

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

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

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

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

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

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

这是错误:

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

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

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

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

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

于 2017-06-25T14:34:37.343 回答