何时使用std::size_t?

269

我只是想知道在循环和其他方面,我应该使用std::size_t还是int?例如:

#include <cstdint>

int main()
{
    for (std::size_t i = 0; i < 10; ++i) {
        // std::size_t OK here? Or should I use, say, unsigned int instead?
    }
}

一般来说,何时使用std::size_t是最佳实践?

15个回答

232

一个好的经验法则是对于需要在循环条件中与自然为std::size_t的某些内容进行比较的任何东西,都应该使用类型本身。

std::size_t是任何sizeof表达式的类型,并且保证能够表示C++中任何对象(包括任何数组)的最大大小。因此,它也被保证足够大来表示任何数组索引,因此是一个循环遍历数组的自然类型。

如果你只是计数到一个数字,那么使用持有该数字的变量的类型或者intunsigned int(如果足够大)可能更自然一些,因为这些类型应该是机器的自然大小。


58
值得一提的是,在应该使用size_t而没有使用时,可能会导致安全漏洞 - BlueRaja - Danny Pflughoeft
7
“int” 不仅是“自然”的,而且混合使用有符号和无符号类型也可能导致安全漏洞。无符号索引很难处理,这也是使用自定义矢量类的一个好理由。 - Jo So
3
@JoSo 还有用于有符号值的 ssize_t - EntangledLoops
1
@EntangledLoops ssize_t并没有size_t的全部范围。它只是size_t所转换的带符号变量。这意味着,使用ssize_t时无法使用内存的全部范围,并且在依赖于size_t类型的变量时可能会发生整数溢出。 - Thomas
1
@Thomas 是的,但我不确定你在表达什么观点。 我只是想说作为'int'的替代品,它更接近语义匹配。你关于'ssize_t'的完整范围不可用的评论是正确的,但'int'也是如此。真正重要的是使用适合应用程序的适当类型。 - EntangledLoops
如果你只是要计数到一个数字,听起来可能很愚蠢,但为什么不使用 std::uintmax_t 呢?它很可能足够大了,如果不够大,那么你无论如何都需要有创造性。编辑:这是 C++11,我刚刚查看了一下,答案是来自2009年。 - Bolpat

80

size_tsizeof运算符的结果类型。

在表示数组大小或索引的变量中使用size_tsize_t传达语义:您立即知道它表示字节大小或索引,而不仅仅是另一个整数。

此外,使用size_t表示字节大小有助于使代码可移植。


38

size_t类型用于指定某个对象的大小,因此在获取字符串长度并处理每个字符时自然而然地使用它:

for (size_t i = 0, max = strlen (str); i < max; i++)
    doSomethingWith (str[i]);

当然,你必须注意边界条件,因为它是无符号类型。顶部的边界通常并不那么重要,因为最大值通常很大(尽管有可能会达到那个值)。大多数人只使用 int 来处理这类问题,因为他们很少有超过该 int 容量的结构体或数组。

但要注意以下情况:

for (size_t i = strlen (str) - 1; i >= 0; i--)

由于无符号值的包装行为(尽管我已经看到编译器对此发出警告),会导致无限循环。这也可以通过以下较难理解但至少不会受到包装问题影响的方式来缓解:

for (size_t i = strlen (str); i-- > 0; )

将递减操作转移到继续条件的后置检查副作用中,可以在递减之前对值进行继续检查,但仍然在循环内使用递减后的值(这就是为什么循环从len .. 1而不是len-1 .. 0运行的原因)。


25
顺便提一下,在循环中每次调用strlen不是一个好习惯。您可以像这样做:for(size_t i = 0, len = strlen(str); i < len; i++) ... - musiphil
1
即使它是有符号类型,你仍然需要注意边界条件,甚至更加重要,因为有符号整数溢出是未定义的行为。 - Adrian McCarthy
3
正确地倒数可以通过以下(臭名昭著的)方式完成:for (size_t i = strlen(str); i --> 0;) - Jo So
1
@JoSo,这实际上是一个相当巧妙的技巧,但我不确定是否喜欢引入“-->”“转到”运算符(请参见https://dev59.com/RXI-5IYBdhLWcg3w18d3)。已将您的建议纳入答案。 - paxdiablo
你能在for循环的末尾加上一个简单的 if (i == 0) break; 吗?例如: for (size_t i = strlen(str) - 1; ; --i)。(我更喜欢你的方法,只是想知道这个方法是否同样有效)。 - RastaJedi

14

简短回答:

几乎从不。使用带符号的版本ptrdiff_t或非标准的ssize_t。使用函数std::ssize代替std::size

长回答:

只有在32位系统上需要一个大于2GB的char向量时才需要使用无符号类型。在其他所有情况下,使用带符号类型比使用无符号类型更安全。

例如:

std::vector<A> data;
[...]
// calculate the index that should be used;
size_t i = calc_index(param1, param2);
// doing calculations close to the underflow of an integer is already dangerous

// do some bounds checking
if( i - 1 < 0 ) {
    // always false, because 0-1 on unsigned creates an underflow
    return LEFT_BORDER;
} else if( i >= data.size() - 1 ) {
    // if i already had an underflow, this becomes true
    return RIGHT_BORDER;
}

// now you have a bug that is very hard to track, because you never 
// get an exception or anything anymore, to detect that you actually 
// return the false border case.

return calc_something(data[i-1], data[i], data[i+1]);
size_t的有符号等价类型是ptrdiff_t,而不是int。但在大多数情况下,使用int仍然比size_t更好。在32位和64位系统上,ptrdiff_tlong类型。
这意味着每次与std::containers交互时,您都必须进行大小转换,这并不美观。但在一个going native的会议上,C++的作者提到将std::vector设计为无符号的size_t是一个错误。
如果编译器在从ptrdiff_tsize_t的隐式转换方面发出警告,则可以使用构造函数语法使其明确。
calc_something(data[size_t(i-1)], data[size_t(i)], data[size_t(i+1)]);

如果只想迭代一个集合,而不需要检查边界,请使用基于范围的for循环:

for(const auto& d : data) {
    [...]
}

以下是C++之父Bjarne Stroustrup在“going native”演讲中的一些话:(原文链接)

对于一些人来说,STL中的签名/无符号设计错误足以成为不使用std::vector而使用自己的实现的原因。


2
我理解他们的想法,但我仍然认为写for(int i = 0; i < get_size_of_stuff(); i++)很奇怪。当然,你可能不想做很多原始循环,但是 - 来吧,你也会使用它们。 - einpoklum
我使用原始循环的唯一原因是,C++算法库设计得相当糟糕。有些语言(如Scala)拥有更好、更先进的集合操作库,这时使用原始循环的用例就几乎被消除了。也有一些方法可以通过新的、更好的STL来改进C++,但我怀疑这在未来十年内不会发生。 - Arne
1
我明白这段代码:unsigned i = 0; assert(i-1, MAX_INT);但我不理解你为什么说“如果i已经发生了下溢,那么这个表达式就会成立”,因为无符号整数的算术行为总是被定义的,即结果是最大可表示整数大小的模。所以如果i==0,则i--变成MAX_INT,然后i++又变成0。 - mabraham
@mabraham 我仔细看了一下,你是对的,我的代码不是最好的展示问题的方式。通常情况下,x + 1 < y 等同于 x < y - 1,但是对于无符号整数来说并非如此。这很容易在转换时引入错误,因为假定它们是等价的。 - Arne

14

size_t是根据sizeof运算符计算的结果,被用于表示大小。

在您的示例中,您执行某些操作的次数(即10)与大小无关,所以为什么要使用size_t呢?使用intunsigned int应该可以。

当然,您在循环内部对i执行的操作也很重要。例如,如果将其传递给需要unsigned int的函数,则选择unsigned int

无论如何,我建议避免隐式类型转换。 让所有类型转换都显式化。


10

size_t是一种非常易读的方式来指定一个项目的尺寸维度——例如字符串长度、指针占用的字节数等。此外,它在不同平台上也很便携——你会发现64位和32位都可以很好地与系统函数和 size_t 一起使用,而使用unsigned int可能不会这样(例如,何时应使用 unsigned long)。


9

在索引/计数C风格数组时,请使用std::size_t。

对于STL容器,您将拥有(例如)vector<int>::size_type,应该用于索引和计数向量元素。

实际上,它们通常都是无符号整数,但并不保证,特别是在使用自定义分配器时。


2
在Linux上使用gcc编译时,std::size_t通常是 unsigned long,而不是unsigned int(在64位系统上为8个字节)。 - rafak
5
C风格的数组并非通过size_t进行索引,因为索引可以是负数。如果不希望出现负数索引,可以对自己定义的此类数组使用size_t进行索引。请注意,这并不改变数组本身的特性。 - Johannes Schaub - litb
2
由于C风格的数组索引等同于在指针上使用运算符“+”,因此似乎应该使用ptrdiff_t作为索引。 - Pavel Minaev
8
关于vector<T>::size_type(以及其他所有容器),它实际上是相当无用的,因为它基本上保证为size_t - 它被typedef为Allocator::size_type,并且对于与容器相关的其余限制,请参见20.1.5/4 - 特别是,size_type必须是size_tdifference_type必须是ptrdiff_t。当然,默认的std::allocator<T>满足这些要求。所以只需使用更短的size_t,不用关心其他东西 :) - Pavel Minaev
1
我必须评论一下C风格数组和负索引。是的,你可以,但你不应该。访问数组边界之外是未定义的。如果你正在使用指针进行棘手的操作,用数组索引而不是指针数学(以及大量的代码注释)来完成这个混乱的想法是一个令人困惑的坏主意。 - Zan Lynx
显示剩余2条评论

7

很快,大部分计算机都将采用64位架构,并运行在操作数以亿计的容器上的64位操作系统。因此,您必须使用size_t而不是int作为循环索引,否则您的索引将在第2^32个元素处回绕,无论是在32位还是64位系统上。

为未来做好准备!


1
你的论点只是意味着需要一个 long int 而不是一个 int。如果在 64 位操作系统上 size_t 是相关的,那么在 32 位操作系统上它同样重要。 - einpoklum

6

size_t是各种库返回的一个指示容器大小非零的数据类型。当你收到0时,使用它。

然而,在你上面的示例中循环size_t可能会导致潜在的错误。考虑以下情况:

for (size_t i = thing.size(); i >= 0; --i) {
  // this will never terminate because size_t is a typedef for
  // unsigned int which can not be negative by definition
  // therefore i will always be >= 0
  printf("the never ending story. la la la la");
}

使用无符号整数有可能导致这些微妙的问题。因此,在我看来,只有与需要它的容器/类型交互时才喜欢使用size_t。


1
每个人似乎都在循环中使用size_t,而不必担心这个错误,我是通过艰难的方式学到的。 - Pranjal Gupta

4

使用size_t时要小心以下表达式

size_t i = containner.find("mytoken");
size_t x = 99;
if (i-x>-1 && i+x < containner.size()) {
    cout << containner[i-x] << " " << containner[i+x] << endl;
}

无论x的值是多少,if表达式中都将返回false。我花了几天时间才意识到这一点(代码非常简单,所以我没有进行单元测试),虽然只需要几分钟就可以找到问题的根源。不确定是更好地进行强制转换还是使用零。
if ((int)(i-x) > -1 or (i-x) >= 0)

两种方式都可以。这是我的测试结果。
size_t i = 5;
cerr << "i-7=" << i-7 << " (int)(i-7)=" << (int)(i-7) << endl;

输出结果:i-7=18446744073709551614 (int)(i-7)=-2
我希望能听到其他人的意见。

2
请注意,(int)(i - 7) 是一个下溢,之后被转换为 int,而 int(i) - 7 不是下溢,因为你首先将 i 转换为 int,然后再减去 7。此外,我发现你的示例令人困惑。 - hochl
我的观点是,在进行减法运算时,int类型通常更安全。 - Kemin Zhou

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