是否仍然有理由在C++代码中使用"int"

Is there still a reason to use `int` in C++ code?

本文关键字:quot int 代码 有理由 C++ 是否      更新时间:2023-10-16

许多风格指南(例如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

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

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

从我所看到的,唯一使int与众不同的似乎是这样一个事实,例如5int的,但我看不出如果我们移动到std::ptrdiff_t作为默认整数可能会导致任何问题。

我即将std::ptrdiff_t作为我小公司编写的所有代码的实际标准整数。有理由说明这可能是一个糟糕的选择吗?

PS:我同意std::ptrdiff_t这个名字很丑的事实,这就是为什么我把它打字定义il::int_t看起来更好一点的原因。

PS:我知道很多人会建议我使用std::size_t作为默认整数,我真的很想明确表示,我不想使用无符号整数作为默认整数。在STL中使用std::size_t作为默认整数是一个错误,正如Bjarne Stroustrup和标准委员会在视频互动小组:在42:38和1:02:50询问我们任何事情中所承认的那样。

PS:在性能方面,在我所知道的任何64位平台上,+-*的编译方式与intstd::ptrdiff_t相同。所以速度没有区别。如果除以编译时常量,则速度是相同的。只有当你a/b除法时,当你对b一无所知时,在 64 位平台上使用 32 位整数才能在性能上略有优势。但这种情况非常罕见,因为我不认为这是远离std::ptrdiff_t的选择。当我们处理矢量化代码时,这里有明显的区别,越小越好,但那是另一回事,没有理由坚持int.在这些情况下,我建议使用固定大小的C++类型。

有一个关于C++核心指南的讨论,使用什么:

https://github.com/isocpp/CppCoreGuidelines/pull/1115

赫伯·萨特(Herb Sutter)写道,将添加gsl::index(将来可能会std::index),这将被定义为ptrdiff_t

hsutter 评论于 2017-12-26 •

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

将以下类型定义添加到 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()上发出有符号/无符号不匹配(类似i!=v.size())适用于今天的STL容器。(如果将来的 STL 更改其 size_type被签署,即使是最后一个缺点也会消失。

然而,尝试教书是没有希望的(和尴尬的) 人们经常写for (ptrdiff_t i = ... ; ... ; ...).(甚至 《指南》目前只在一个地方使用它,这是一个"坏" 与索引无关的示例。

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

为什么不直接告诉人们写ptrdiff_t因为我们相信 告诉人们这就是你必须做的事情会很尴尬 C++,即使我们这样做了,人们也不会这样做。写ptrdiff_t也是 与autoint相比,丑陋且不可收养.添加 名称index是为了使使用尽可能简单和有吸引力的 大小正确的签名类型。

编辑:来自Herb Sutter的更多理由

ptrdiff_t够大吗?是的。已经需要标准容器 没有比ptrdiff_t表示的更多的元素,因为 减去两个迭代器必须适合一个difference_type。

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

我从一个老计时器的角度(C++之前)来的......当时人们知道int是平台的本地词,可能会提供最佳性能。

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

但是现在,今天,处理器和编译器要复杂得多,这些规则并不那么容易应用。也很难预测您的选择对某些未知的未来平台或编译器的性能影响...... 例如,我们如何真正知道uint64_t在任何特定的未来目标上的表现会比uint32_t更好或更差?除非您是处理器/编译器专家,否则您不会...

所以......也许它是老式的,但除非我为Arduino等受限环境编写代码。我仍然将int用于通用值,我知道这些值在我正在编写的应用程序的所有合理目标上都在int大小范围内。 编译器从那里获取它... 如今,这通常意味着 32 位签名。 即使假设 16 位是最小的整数大小,它也涵盖了大多数用例。以及大于该数字的用例很容易识别并使用适当的类型进行处理。

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

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

也就是说,打字int感觉就像你刚刚对里奇说嘿,打字std::ptrdiff_t感觉就像斯特劳斯特鲁普只是踢了你的屁股。程序员也是人,不会给他们的生活带来太多的丑陋。我更喜欢使用long一些易于键入的typedef,例如index而不是std::ptrdiff_t

这有点基于意见,但唉,这个问题也有些乞求它。

首先,你谈论整数和索引,就好像它们是一回事一样,但事实并非如此。对于任何诸如"整数,不确定大小">之类的东西,简单地使用int当然,大多数时候,仍然是合适的。对于大多数应用程序,这在大多数情况下都工作正常,并且编译器对此感到满意。默认情况下,这很好。

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

到目前为止,只有一件形式上正确的事情,那就是std::size_t。将来,可能会有一个std::index_t使意图在源代码级别上更加清晰,但到目前为止还没有。
std::ptrdiff_t作为索引"有效",但与int一样不正确,因为它允许负索引。
是的,这是萨特先生认为正确的,但我不同意。是的,在汇编语言指令级别上,这得到了很好的支持,但我仍然反对。该标准说:

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

数组订阅是指E1的第E2个成员。数组中没有负数元素这样的东西。但更重要的是,具有负加法表达式的指针算术调用未定义的行为

换句话说:无论大小的符号索引都是错误的选择。索引是无符号的。是的,签名索引有效,但它们仍然是错误的。

现在,尽管根据定义size_t是正确的选择(一种无符号整数类型,其大小足以包含任何对象的大小),但它对于普通情况是否真正是好的选择,或者作为默认值,可能是值得商榷的。

老实说,您上一次创建包含 10 个19 个元素的数组是什么时候?

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

如果您正在编写关系数据库或文件系统,那么是的,您将需要64 位索引。但对于普通的"普通"程序来说,32位索引就足够了,而且它们只消耗一半的存储空间。

当保留超过少数索引时,如果我负担得起(因为数组不大于 64k 元素),我什至会下降到uint16_t.不,我不是在开玩笑。

存储真的是这样的问题吗?贪婪地保存两四个字节是荒谬的,不是吗!嗯,不...

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

但是,例如考虑一个插槽映射,您可以在其中存储每个元素的索引(取决于实现,每个元素两个索引)。哦,哎呀,无论您每次都点击 L2,还是每次访问都有缓存未命中,它肯定会产生令人沮丧的区别!越大并不总是越好。

归根结底,你必须问问自己你付出了什么,你得到了什么回报。考虑到这一点,我的风格建议是:

如果它"不花钱",因为你只有一个指针和几个索引需要保留,那么只需使用形式上正确的(这将是size_t)。形式上正确是好的,正确总是有效的,它是可读和可理解的,正确的是......永远不会错

但是,如果它确实花费了你(你可能有几百、几千或一万个索引),而你得到的东西一文不值(因为例如,你甚至不能存储 220个元素,所以你是否可以订阅 2322 64没有区别),你应该三思而后行,不要太浪费了。

在大多数现代 64 位体系结构上,int为 4 个字节,ptrdiff_t为 8 个字节。 如果程序使用大量整数,则使用ptrdiff_t而不是int可能会使程序的内存需求加倍

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

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

因此int这是一个实际原因它在64位机器上仍然很受欢迎。

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

我给你的建议是不要过多地关注汇编语言输出,不要太担心每个变量的确切大小,也不要说"编译器感觉宾至如归"。 (我真的不知道你说的最后一句是什么意思。

对于花园品种的整数,大多数程序都充满了普通int的整数应该是一个很好的类型。 它应该是机器的自然字大小。 它应该使用起来很有效,在内存和计算寄存器之间移动时,既不会浪费不必要的内存,也不会引起大量额外的转换。

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

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

我认为没有使用int的真正理由。

如何选择整数类型?

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

这些规则涵盖了int的所有可能用法,它们给出了更好的解决方案:

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

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

由于提到的std类型写起来有点乏味,因此可以typedef它们更短(我使用s8/u8/.../s64/u64spt/upt("(无)符号指针大小的类型")用于ptrdiff_t/size_t。我已经使用这些 typedef 15 年了,从那以后我再也没有写过一篇int......

Pro

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

许多 API 使用 int,包括标准库的一部分。 这在历史上会引起问题,例如在过渡到 64 位文件大小期间。

由于默认的类型提升规则,比 int 窄的类型可以扩大到 int 或无符号 int,除非您在很多地方添加显式强制转换,并且许多不同的类型在某些实现中可能比 int 窄。因此,如果您关心便携性,这是一个令人头疼的问题。

缺点

大多数时候,我也使用ptrdiff_t作为索引。 (我同意谷歌的观点,即无符号索引是一个错误吸引子。 对于其他类型的数学,有int_fast64_t.int_fast32_t,等等,也会和int一样好或更好。除了上个世纪一些已停产的Unices之外,几乎没有现实世界的系统使用ILP64,但是有很多CPU需要64位数学运算。 按照标准,如果编译器的int大于 32,767,则在技术上允许编译器中断程序。

也就是说,任何值得一提的 C 编译器都将在大量代码上进行测试,这些代码向内部循环中的指针添加int。 所以它不能做任何太愚蠢的事情。在当今硬件上,最坏的情况是,它需要一个额外的指令来将 32 位有符号值签名扩展到 64 位。 但是,如果你真正想要的是最快的指针数学,幅度在 32 kibi 和 2 gibi 之间的值的最快数学,或者浪费最少的备忘录,你应该说出你的意思,而不是让编译器猜测。

我想 99% 的情况没有理由使用int(或其他大小的有符号整数)。但是,在某些情况下,使用int是一个不错的选择。


A) 性能:

intsize_t之间的一个区别是,i++可以是int的未定义行为 - 如果iMAX_INT。这实际上可能是一件好事,因为编译器可以使用这种未定义的行为来加快速度。

例如,在这个问题中,区别在于利用未定义的行为和使用禁止此漏洞利用的编译器标志-fwrapv之间的因素 2。

如果我的工作马循环通过使用ints 变得快两倍 - 当然我会使用它


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 位之间确实存在差异。