为什么size_t未签名?

时间:2012-04-16 02:30:25

标签: c++ unsigned-integer size-t

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

  

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

size_t 似乎是无符号的“再获得一位代表正整数”。这是一个错误(或权衡),如果是这样,我们应该在我们自己的代码中最小化它的使用吗?

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

4 个答案:

答案 0 :(得分:59)

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)。

答案 1 :(得分:26)

size_tunsigned,因为负面尺寸毫无意义。

(来自评论:)

并不是那么确定,而是说明是什么。你最后一次看到大小为-1的列表是什么时候?遵循该逻辑过多,您会发现根本不存在无符号,也不应允许位操作。 - geekosaur

更重要的是:由于您应该考虑的原因,地址未签名。通过比较地址生成大小;将地址视为已签名将完成错误的操作,并且对结果使用带符号的值会丢失数据,使得您对Stroustrup引用的读取显然认为是可接受的,但事实上并非如此。也许你可以解释负面地址应该做什么。 - geekosaur

答案 2 :(得分: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) { ... }

Yikes,现在我们有未定义的行为!当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。

答案 3 :(得分:1)

另一方面......

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

有两个&#34;历史&#34;通常在这里提到的原因:

  1. sizeof返回std::size_t,自C日以来一直未签名。
  2. 处理器的字长较小,因此重要的是挤出额外的范围。
  3. 但是,尽管这些原因很老,但这些原因实际上都没有归结为历史。

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

    替代方案都更糟糕:您可以禁用已签名/未签名的比较警告和大小转换警告,并希望值始终位于重叠范围内,以便您可以使用可能引入的不同类型忽略潜在错误。或者您可以进行范围检查和显式转换的批次。或者您可以使用巧妙的内置转换来引入自己的大小类型以集中范围检查,但是没有其他库会使用您的大小类型。

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

    我们的新代码仍然需要与标准库互操作。如果我们的新代码使用了签名类型,而标准库继续使用未签名类型,那么我们会让每个必须同时使用这两者的消费者更难。

    神话2 :你不需要额外的一点。 (A.K.A.,当你的地址空间只有4GB时,你永远不会有超过2GB的字符串。)

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

    神话3 :您始终可以使用更宽的签名类型。

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

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

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

    这意味着签名算术并不令人惊讶或不知何故更自然。并且,也许是在用数学思考时,所有基本算术运算都是在所有整数集上闭合的。

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

    这是错误:

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

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

    如果minmax无符号,则总和仍可能溢出,但未定义的行为消失了。你仍然会得到错误的答案,所以它仍然令人惊讶,但不会比签名的内容更令人惊讶。

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

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

相关问题