为什么size_t没有签名
Why is size_t unsigned?
Bjarne Stroustrup在The C++ Programming Language中写道:
无符号整数类型非常适合将存储视为 位数组。使用无符号而不是 int 多获得一位 表示正整数几乎从来都不是一个好主意。尝试 通过声明变量无符号来确保某些值为正值 通常会被隐式转换规则击败。
size_t似乎是无符号的"多获得一位来表示正整数"。那么这是一个错误(或权衡),如果是这样,我们是否应该尽量减少在我们自己的代码中使用它?
斯科特·迈耶斯(Scott Meyers)的另一篇相关文章在这里。总而言之,他建议不要在接口中使用 unsigned ,无论该值是否始终为正数。换句话说,即使负值没有意义,也不一定要使用 unsigned。
由于历史原因,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)。
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。
...
误区 1:std::size_t
未签名是因为不再适用的旧版限制。
这里通常提到的两个"历史"原因:
-
sizeof
返回std::size_t
,自C时代以来一直没有签名。 - 处理器的字数较小,因此挤出额外的范围非常重要。
这两个原因,尽管很古老,但实际上都没有被归入历史。
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
如果对min
和max
进行签名,则总和可能会溢出,从而产生未定义的行为。 我们大多数人经常错过这类错误,因为我们忘记了加法并没有在有符号整数集上关闭。 我们侥幸逃脱,因为我们的编译器通常会生成一些合理(但仍然令人惊讶)的代码。
如果 min
和 max
未签名,则总和仍可能溢出,但未定义的行为已消失。 你仍然会得到错误的答案,所以这仍然令人惊讶,但并不比签名的整数更令人惊讶。
惊喜来自减法:如果你从一个较小的整数中减去一个较大的无符号 int,你最终会得到一个很大的数字。 这个结果并不比除以 0 更令人惊讶。
即使你可以从所有API中消除未签名的类型,如果你处理标准容器或文件格式或有线协议,你仍然必须为这些未签名的"惊喜"做好准备。 是否真的值得在您的 API 中添加摩擦以仅"解决"部分问题?
- 为什么(-1)%vector::size()总是返回0
- 循环中的条件:为什么每次都调用strlen(),而vector.size()只调用一次
- 为什么这个 std::queue/指向结构的指针列表直到 List.Size() == 0 才释放内存?
- 为什么 GCC 不能假设 std::vector::size 在这个循环中不会改变?
- 为什么"(!v.empty())"比"(v.size() >0)"好?
- 为什么gmp会在这里与"invalid next size"重新定位一起崩溃?
- 为什么我会"Invalid read of size 8"?(瓦尔格林德)
- 为什么我不能将 size() 函数编写为 while 循环中的条件?
- 为什么当向量 v 为空时是 (0 < v.size()-1?
- 为什么我必须在初始化 std::array<SomeStruct, size> 时指定每个项目的类型C++
- 为什么会出现错误:size不是std的成员
- 为什么向量不支持 size 作为成员变量,就像 Java 类中的长度变量一样,而是函数 size()?
- 为什么在调用 QXYSeries::replace() 后,当 size() 没有超过 capacity() 时 QVe
- 为什么 0 < string.size() - N 为真,而 string.size() = 0
- 为什么选择 g++ 给予者:"error: cast to pointer from integer of different size [-Werror=int-to-pointer-cast]"
- 为什么 c++ STL 在 size() 函数中使用无符号
- 为什么vector.size() - 1给出垃圾值
- 为什么 --string::end() 可以编译,而 --string.size() 不能编译?
- 为什么 size() 在与其他堆栈交换元素后提供错误的堆栈大小?
- 为什么std :: hex导致内存腐败vector.size()