还有理由在 C + + 代码中使用‘ int’吗?

许多样式指南,比如 Google 推荐在索引数组时使用 int作为默认整数。随着64位平台的崛起,大多数时候 int只有32位,这并不是平台的自然宽度。因此,除了简单的相同之外,我看不出有任何理由保持这种选择。我们清楚地看到,在编写以下代码时:

double get(const double* p, int k) {
return p[k];
}

编译成

movslq %esi, %rsi
vmovsd (%rdi,%rsi,8), %xmm0
ret

其中第一条指令将32位整数提升为64位整数。

如果将代码转换为

double get(const double* p, std::ptrdiff_t k) {
return p[k];
}

生成的程序集现在是

vmovsd (%rdi,%rsi,8), %xmm0
ret

这清楚地表明,与使用 int相比,使用 std::ptrdiff_t的 CPU 感觉更自在。许多 C + + 用户已经迁移到 std::size_t,但是我不想使用无符号整数,除非我真的需要模 2^n行为。

在大多数情况下,使用 int并不会影响性能,因为未定义的行为或有符号整数溢出允许编译器在内部将任何 int提升为处理索引的循环中的 std::ptrdiff_t,但我们从上面清楚地看到,编译器对 int并不熟悉。此外,在64位平台上使用 std::ptrdiff_t将使溢出发生的可能性降低,因为我看到越来越多的人在处理比 2^31 - 1更大的整数时被 int溢出困住,而这种情况在当今变得非常普遍。

从我所看到的来看,使 int独立的唯一原因似乎是像 5这样的字面值是 int,但是我不认为如果我们将 std::ptrdiff_t作为默认整数移动到 std::ptrdiff_t会导致任何问题。

我的小公司写的所有代码都将使用行业标准整数 std::ptrdiff_t。有什么理由说这是个错误的选择吗?

PS: 我同意这个事实,名字 ABC0是丑陋的,这就是为什么我已经把它的类型定义为 il::int_t看起来好一点。

PS: 因为我知道很多人会建议我使用 std::size_t作为默认整数,我真的想说清楚,我不想使用无符号整数作为我的默认整数。使用 std::size_t作为 STL 的默认整数是一个错误,比雅尼·斯特劳斯特鲁普和标准委员会在视频 互动面板: 问我们任何问题时间42:38和1:02:50都承认了这一点。

附注: 在性能方面,据我所知,在任何64位平台上,ABC0、 ABC1和 ABC2的编译方法对于 int和 ABC4都是相同的。所以在速度上没有区别。如果除以编译时常数,速度是相同的。只有在对 ABC6一无所知的情况下除以 ABC5,才能在64位平台上使用32位整数使您在性能方面略有优势。但这种情况是如此罕见,因为我没有看到一个选择从 ABC4移开。当我们处理矢量化代码时,这里有一个明显的区别,而且越小越好,但是这是一个不同的故事,没有理由坚持使用 int。在这些情况下,我建议使用固定大小的 C + + 类型。

25118 次浏览

大多数程序不会在几个 CPU 周期的边缘生存或死亡,而且 int非常容易编写。但是,如果您对性能敏感,我建议使用在 <cstdint>中定义的固定宽度整数类型,如 int32_tuint64_t。 这样做的好处是,可以非常清楚地看到它们在签名或未签名方面的预期行为,以及它们在内存中的大小。这个头还包括快速变体,如 int_fast32_t,这是 至少的规定大小,但可能更多,如果它有助于性能。

会上讨论了 C + + 核心指南的使用方法:

Https://github.com/isocpp/cppcoreguidelines/pull/1115

Herb Sutter 写道 gsl::index将被添加(将来可能是 std::index) ,它将被定义为 ptrdiff_t

Hsutter 于2017年12月26日发表评论•

(感谢许多 WG21专家对此的评论和反馈 注释)

将以下 typedef 添加到 GSL

namespace gsl { using index = ptrdiff_t; }

并推荐所有容器索引/下标/大小的 gsl::index

理由

指南建议对下标/索引使用签名类型。 参见 ES.100到 ES.107。 C + + 已经在数组中使用了有符号整数 下标。

我们希望能够教人们编写“新的干净的现代代码” 这是简单的,自然的,警告,免费在高警告级别,以及 不会让我们写一个关于简单代码的“陷阱”脚注。

如果我们没有一个简短的可采用的词,如 index,这是竞争力 有了 intauto,人们仍然会使用 intauto,并得到他们的 例如,他们会写 for(int i=0; i<v.size(); ++i)for(auto i=0; i<v.size(); ++i)有32位大小的错误广泛存在 使用的平台,和 for(auto i=v.size()-1; i>=0; ++i)只是 没有用。我不认为我们可以教 for(ptrdiff_t i = ... 面不改色,否则人们会接受的。

如果我们有一个饱和算术类型,我们可能会使用它。否则, 最好的选择是 ptrdiff_t,它几乎具有所有的优点 饱和算术无符号类型,除了 ptrdiff_t仍然 使得普及循环风格 for(ptrdiff_t i=0; i<v.size(); ++i)i<v.size()上发出有符号/无符号的不匹配(对于 (如果将来的 STL 更改了它的 Size _ type 进行签名,即使最后一个缺点也消失了。)

然而,试图教书是没有希望的(也是令人尴尬的) 人们经常写 for (ptrdiff_t i = ... ; ... ; ...)。(甚至 指南目前只在一个地方使用,这是一个“坏” 与索引无关的例子。)

因此,我们应该提供 gsl::index(稍后可以提出 考虑作为 std::index)作为 ptrdiff_t的 typedef,所以我们可以 希望(而不是令人尴尬地)教人们经常写作 (index i = ... ; ... ; ...).

为什么不直接告诉人们写 ptrdiff_t? 因为我们相信它 要是告诉别人这是你必须做的,会很尴尬的 C + + ,即使我们这样做了,人们也不会这样做。编写 ptrdiff_t也是如此 与 autoint相比,它们丑陋且不可采用 名称 index是为了使它尽可能容易和有吸引力地使用一个 大小正确的签名字体。

编辑: Herb Sutter 提供的更多理论依据

ptrdiff_t够大吗? 是的。已经要求标准容器 没有比 ptrdiff_t所能表示的更多的元素,因为 减去两个迭代器必须适合差异 _ 类型。

但是如果我有一个内置的 char数组,那么 ptrdiff_t真的足够大吗 或者大于内存地址空间一半大小的 byte 所以有比在 ptrdiff_t中表示的更多的元素? Yes。 C + + 已经在数组下标中使用了有符号整数 默认选项的绝大多数用途,包括所有 内置数组(如果遇到极其罕见的 数组或类似数组的类型,其大于地址空间的一半 它的元素是 sizeof(1)你要小心避免 截断问题,继续并使用 size_t作为其中的索引 非常特殊的容器。这种动物在实践中非常罕见, 当它们出现时,通常不会被用户代码直接索引。 例如,它们通常出现在接管的内存管理器中 系统分配和分配出个别较小的分配 用户使用,或在 MPEG 或类似的,提供自己的 接口; 在这两种情况下,size_t应该只在内部需要 在内存管理器或 MPEG 类实现中。)

没有使用 int的正式理由。它不符合任何正常的标准。对于索引,几乎总是需要有符号指针大小的整数。

也就是说,键入 int感觉就像你刚刚对 Ritchie 打了个招呼,键入 std::ptrdiff_t感觉就像 Stroustrup 刚刚踢了你的屁股。程序员也是人,不要给他们的生活带来太多的丑陋。我更喜欢使用 long一些易于类型化的 typedef,如 index而不是 std::ptrdiff_t

我认为使用 int没有 真的的原因。

如何选择整数类型?

  • 如果是用于位操作,则可以使用无符号类型,否则使用有符号类型
  • 如果是与内存相关的东西(索引、容器大小等) ,而您不知道它的上限,那么使用 std::ptrdiff_t(唯一的问题是当大小大于 PTRDIFF_MAX时,这种情况在实践中很少见)
  • 否则使用 intXX_tint(_least)/(_fast)XX_t

这些规则涵盖了 int的所有可能用法,并提供了更好的解决方案:

  • int不适合存储与内存相关的东西,因为它的范围可能比索引小(这不是理论上的东西: 对于64位机器,int通常是32位的,所以使用 int,你只能处理20亿个元素)
  • int不适合存储“一般”整数,因为它的范围可能小于需要的未定义行为(如果范围不够大,就会出现这种情况) ,或者相反,它的范围可能大于需要的范围(因此内存被浪费)

如果进行了计算,并且知道该范围适合[-32767; 32767](标准只保证该范围) ,那么使用 int的唯一原因是。但是请注意,实现可以自由地提供更大的 int,而且它们通常也这样做。目前很多平台上的 int都是32位的)。

由于上面提到的 std类型编写起来有点繁琐,可以将 typedef类型缩短(我使用 s8/u8/.../s64/u64,而 spt/upt(“(un)有符号指针大小的类型”)用于 ptrdiff_t/size_t。我已经使用这些 typedef 15年了,从那以后我再也没有写过一个 typedef0。

我的建议是,不要过多地关注汇编语言的输出,不要过多地担心每个变量的确切大小,也不要说“编译器感觉很自在”之类的话。(我真的不知道你说的最后一个是什么意思。)

对于大多数程序都充满的普通整数,普通 int被认为是一种很好的使用类型。它应该是机器的自然单词大小。它应该是高效使用,既不浪费不必要的内存,也不会在内存和计算寄存器之间移动时产生大量额外的转换。

现在,确实有许多更加专门化的用途,但是普通的 int已经不再适合了。特别是,对象的大小、元素的计数和数组的索引几乎总是 size_t。但这并不意味着所有的整数都应该是 size_t

有符号和无符号类型的混合以及不同大小类型的混合也会导致问题。但是现代编译器和它们对不安全组合发出的警告已经很好地处理了其中的大多数问题。因此,只要使用现代编译器并注意它的警告,就不需要仅仅为了避免类型不匹配问题而选择非自然类型。

我是从一个老式计时器(pre C + +)的角度来看待这个问题的... ... 在那个时代,人们理解 int是平台的母语,可能会提供最好的性能。

如果你需要更大的东西,那么你会使用它,并为性能付出代价。如果您需要更小的内存(有限的内存,或者需要固定大小的内存) ,也是一样的。.否则使用 int。是的,如果您的值在一个目标平台上的 int 可以容纳它的范围内,而另一个目标平台上的 int 不能。.然后,我们有了自己的编译时间大小特定定义(在它们变得标准化之前,我们自己做了定义)。

但是现在,处理器和编译器要复杂得多,这些规则不那么容易适用。预测你的选择对某些未知的未来平台或编译器的性能影响也更加困难... 我们如何真正知道比如 uint64 _ t 在任何特定的未来目标上会比 uint32 _ t 表现得更好还是更差?除非你是个处理器/编译器专家,否则你不会..。

所以... 也许这是过时的,但是除非我为一个像 Arduino 这样的受限环境编写代码。我仍然使用 int作为通用值,我知道在我编写的应用程序的所有合理目标上,这些值都在 int大小之内。编译器从这里开始... 现在通常意味着32位有符号。即使假设16位是最小整数大小,它也涵盖了大多数用例。.以及数字大于这些数字的用例,这些用例很容易识别并用适当的类型处理。

赞成

更容易打字,我猜? 但你总是可以 typedef

许多 API 使用 int,包括部分标准库。这在历史上造成了一些问题,例如在转换到64位文件大小的过程中。

由于默认的类型提升规则,除非在很多地方添加显式强制转换,否则比 int 更窄的类型可以扩展为 int 或 unsignedint,而且在某些实现中,很多不同的类型可以比 int 更窄。因此,如果您关心可移植性,这是一个小问题。

反对

在大多数情况下,我还使用 ptrdiff_t作为索引。(我同意谷歌的观点,即未签名索引是一个漏洞吸引器。)对于其他类型的数学,有 int_fast64_tint_fast32_t,等等,这也将是一样好,或优于 int。几乎没有现实世界中的系统使用 ILP64,除了上个世纪的几个不存在的 Unice,但是有很多 CPU 需要64位数学。按照标准,如果 int大于32,767,编译器在技术上是允许中断程序的。

也就是说,任何一个称职的 C 编译器都将在大量代码上进行测试,这些代码将 int添加到内部循环中的指针上。所以它不能做太蠢的事。绝境求生手册 现在的硬件需要额外的指令将32位有符号值符号扩展到64位。但是,如果你真正想要的是最快的指针数学,最快的数值大小在32kibi 和2gibi 之间的数学,或者最少浪费的记忆体,你应该说你的意思,而不是让编译器猜测。

在大多数现代64位架构中,int为4字节,ptrdiff_t为8字节。如果您的程序使用大量的整数,使用 ptrdiff_t而不是 int可以 双倍您的程序的内存需求。

还要考虑到现代 CPU 经常受到内存性能的瓶颈。使用8字节整数还意味着 CPU 缓存现在的元素数量只有以前的一半,因此现在它必须更频繁地等待缓慢的主内存(这很容易占用几百个周期)。

在许多情况下,执行“32到64位转换”操作的成本与内存性能相比完全相形见绌。

因此,这是 int仍然在64位机器上流行的一个实际原因。

  • 现在你可能会争论二十多种不同的整数类型、可移植性、标准委员会等等,但事实是,对于很多写在那里的 C + + 程序来说,他们考虑的是一种“规范”架构,而这通常是他们唯一关心的架构。(如果您正在为 Windows 游戏编写3D 图形例程,那么您肯定它不会在 IBM 大型机上运行。)因此,对他们来说,问题归结为: “我需要一个4字节的整数还是一个8字节的整数?”

这在某种程度上是基于观点的,但遗憾的是,这个问题在某种程度上也需要这样做。

首先,你谈论整数和索引就好像它们是一回事,但事实并非如此。对于任何像 “整数排序,不确定大小”这样的东西,简单地使用 int当然在大多数情况下仍然是合适的。对于大多数应用程序来说,这在大多数情况下都能很好地工作,编译器对此也很满意。默认情况下,没关系。

对于数组索引,情况就不同了。

迄今为止,只有一件事情是正确的,那就是 std::size_t。在未来,可能会有一个 std::index_t,使意图在源级别上更加清晰,但是到目前为止还没有。
std::ptrdiff_t作为一个索引“工作”,但正如不正确的 int,因为它允许负的索引。
是的,这是萨特先生认为正确的事但我不同意。是的,在汇编语言指令级别,这是支持的很好,但我仍然反对。标准规定:

8.3.4/6: E1[E2]*((E1)+(E2))相同[ ... ]由于适用于 +的转换规则,如果 E1是一个数组,而 E2是一个整数,那么 E1[E2]指的是 E1的第4个成员。
5.7/5: [ ... ]如果指针操作数和结果都指向同一数组对象的元素,或者指向数组对象的最后一个元素[ ... ]之后的元素,则为 行为是未定义的

数组订阅引用 E1的第十个成员。数组中没有负的第三个元素。但更重要的是,指针算法具有负的加法表达式 引起未定义行为

换句话说: 任何大小的有符号索引都是 错误的选择。索引是未签名的。是的,签名索引 工作,但他们仍然是错误的。

现在,虽然从定义上来说,size_t是正确的选择(一个大到足以包含任何对象大小的无符号整数类型) ,但是对于一般情况来说,它是 真的的好选择,还是作为默认选择,可能存在争议。

说实话,上一次创建包含1019元素的数组是什么时候?

我个人使用 unsigned int作为默认值,因为它允许的40亿个元素对于(几乎)每个应用程序来说都足够了,而且它已经把普通用户的计算机推到了接近它的极限(如果只是订阅一个整数数组,那就假设分配了16GB 的连续内存)。我个人认为,对64位索引违约是荒谬的。

如果你正在编写一个关系数据库或文件系统,那么是的,你会编写 需要64位索引。但是对于一般的“普通”程序来说,32位索引就足够了,它们只占用一半的存储空间。

当保留大量的索引时,如果我能负担得起(因为数组不超过64k 个元素) ,我甚至会使用 uint16_t。不,我不是在开玩笑。

存储真的有这么大的问题吗? 节省两到四个字节太可笑了,不是吗。

指针的大小可能是个问题,所以对于索引来说也是如此。X32 ABI 并不是无缘无故就存在的。如果总共只有几个不必要的大型索引,您就不会注意到它们的开销(就像指针一样,它们无论如何都会在寄存器中,没有人会注意它们的大小是4字节还是8字节)。

但是考虑一个槽映射的例子,其中存储每个元素的索引(取决于实现,每个元素的 索引)。哦,见鬼,无论你是每次都命中 L2,还是每次访问都有缓存丢失,这确实会产生很大的不同!越大并不总是越好。

在一天结束的时候,你必须问问你自己你付出了什么,你得到了什么回报。考虑到这一点,我的风格建议是:

如果因为只有一个指针和几个索引可以保留,所以不需要花费任何成本,那么只需要使用形式上正确的内容(那就是 size_t)。形式上正确是好的,正确总是有效的,它是可读的和清晰的,正确是... 从没错过

然而,如果它是 是要花钱的你(你可能有几百个、几千个或者一万个索引) ,而你得到的东西一文不值(因为你甚至不能存储220元素,所以无论你 可以订阅232还是264都没有区别) ,你应该再三考虑一下是否太浪费了。

我猜99% 的情况下没有理由使用 int(或其他大小的有符号整数)。然而,仍然有一些情况,当使用 int是一个很好的选择。


A)表演:

intsize_t的一个不同之处在于,如果 iMAX_INT,那么 i++可以作为 int的未定义行为。这实际上可能是一件好事,因为编译器可以使用这个未定义行为来加快处理速度。

例如,在这个 有个问题中,利用未定义行为和使用禁止这种利用的编译器标志 -fwrapv之间的差别大约是因子2。

如果使用 int使我的循环工作速度提高了一倍,那么我肯定会使用它


B)较少错误倾向的代码

使用 size_t的反向 for 循环看起来很奇怪,是错误的来源(我希望我没有弄错) :

for(size_t i = N-1; i < N; i--){...}

通过使用

for(int i = N-1; i >= 0; i--){...}

你应该得到经验不足的 C + + 程序员的感激,他们总有一天会管理你的代码。


C)使用有符号索引进行设计

通过使用 int作为索引,你可以用负值表示错误的值/超出范围的值,这样做很方便,可以得到更清晰的代码。

  1. 如果元素不存在,“查找数组中元素的索引”可以返回 -1。为了检测这个“错误”,你不必知道数组的大小。

  2. 如果元素在数组中,二进制搜索可以返回正索引,对于元素插入到数组中的位置(并且不在数组中)返回 -index

显然,相同的信息可以用正索引值进行编码,但是代码变得不那么直观了。


显然,选择 int而不选择 std::ptrdiff_t也是有原因的——其中之一就是内存带宽。有很多内存限制算法,对于它们来说,减少从 RAM 到缓存的内存量是非常重要的。

如果你知道,所有的数字都小于 2^31,这将是一个优势,使用 int,因为否则一半的内存传输将只写 0,你已经知道,他们在那里。

一个例子是压缩的稀疏行(crs)矩阵-它们的索引存储为 ints而不是 long long。因为许多带有稀疏矩阵的操作都是内存绑定的,所以使用32位和64位确实有所不同。