另一方面 ...
误区 1: std::size_t
未签名是因为不再适用的遗留限制。
这里通常提到两个“历史”原因:
sizeof
Returns std::size_t
,自 C 时代以来一直未签名。
- 处理器的字长更小,因此挤出额外的范围很重要。
但是,这些原因,尽管已经很老了,但实际上都没有成为历史。
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
如果min
和max
被签名,总和可能会溢出,这会产生未定义的行为。我们大多数人经常会错过这些类型的错误,因为我们忘记了加法不是封闭在有符号整数集上的。我们侥幸逃脱,因为我们的编译器通常会生成一些合理的代码(但仍然令人惊讶)。
如果min
和max
是无符号的,总和仍可能溢出,但未定义的行为消失了。你仍然会得到错误的答案,所以它仍然令人惊讶,但并不比使用有符号整数更令人惊讶。
真正的 unsigned 惊喜来自于减法:如果你从一个较小的 unsigned int 中减去一个较大的 unsigned int,你最终会得到一个很大的数字。这个结果并不比你除以 0 更令人惊讶。
即使您可以从所有 API 中消除无符号类型,如果您处理标准容器或文件格式或有线协议,您仍然必须为这些无符号“惊喜”做好准备。是否真的值得在您的 API 中添加摩擦以仅“解决”部分问题?