当 float 不能代表所有 int 值时,为什么 C++ 将 int 提升为 float?

Why does C++ promote an int to a float when a float cannot represent all int values?

假设我有以下内容:

int i = 23;
float f = 3.14;
if (i == f) // do something

i 将被提升为 float 并且将比较两个 float 数字,但是 float 可以代表所有 int 值吗?为什么不将 intfloat 都提升为 double

Q1:float 可以代表所有的 int 值吗?

IEE754 可以将所有整数精确表示为浮点数,最多约 223,如本 answer.

中所述

问题 2:为什么不将 int 和 float 都提升为 double?

The rules in the Standard for these conversions are slight modifications of those in K&R: the modifications accommodate the added types and the value preserving rules. Explicit license was added to perform calculations in a “wider” type than absolutely necessary, since this can sometimes produce smaller and faster code, not to mention the correct answer more often. Calculations can also be performed in a “narrower” type by the as if rule so long as the same end result is obtained. Explicit casting can always be used to obtain a value in a desired type.

Source

以更宽的类型执行计算意味着给定 float f1;float f2;f1 + f2 可能会以 double 精度计算。这意味着给定 int i;float f;i == f 可能会以 double 精度计算。但是,正如 hvd 在评论中所述,不需要以双精度计算 i == f

C标准也是这么说的。这些被称为通常的算术转换。以下描述直接取自 ANSI C 标准。

...if either operand has type float , the other operand is converted to type float .

Source and you can see it in the ref也是。

一个相关的link是这个answer. A more analytic source is here

这是另一种解释方式:通常的算术转换是隐式执行的,以将它们的值转换为通用类型。编译器首先执行整数提升,如果操作数仍然具有不同的类型,则将它们转换为在以下层次结构中出现最高的类型:

Source.

甚至 double 也可能无法表示所有 int 值,这取决于 int 包含多少位。

为什么不将 int 和 float 都提升为 double?

可能是因为将两种类型都转换为 double 比使用其中一个操作数(已经是 floatfloat 成本更高。它还将为与算术运算符规则不兼容的比较运算符引入特殊规则。

也无法保证浮点类型将如何表示,因此假设将 int 转换为 double(甚至 long double)进行比较将是一个盲目的选择什么都会解决。

类型提升规则旨在简单并以可预测的方式工作。 C/C++ 中的类型自然是 "sorted" 由 range of values they can represent. See this 详细说明。虽然浮点类型不能表示整数类型表示的所有整数,因为它们不能表示相同数量的有效数字,但它们可能可以表示更广泛的范围。

为了具有可预测的行为,当需要类型提升时,数字类型总是转换为具有 较大 范围的类型,以避免较小的溢出。想象一下:

int i = 23464364; // more digits than float can represent!
float f = 123.4212E36f; // larger range than int can represent!
if (i == f) { /* do something */ }

如果向整数类型转换,float f 转换为int 时肯定会溢出,导致未定义的行为。另一方面,将 i 转换为 f 只会导致精度损失,这是无关紧要的,因为 f 具有相同的精度,因此比较仍有可能成功。届时由程序员根据应用程序要求解释比较结果。

最后,除了双精度浮点数在表示整数时遇到相同的问题(有效数字的数量有限)之外,对这两种类型使用提升会导致 i 具有更高的精度表示,而 f 注定具有原始精度,因此如果 i 的有效数字比 f 开头的数字多,比较将不会成功。现在这也是未定义的行为:比较可能对某些夫妇 (i,f) 成功,但对其他夫妇则不然。

创建编程语言时,一些决定是凭直觉做出的。

例如,为什么不将 int+float 转换为 int+int 而不是 float+float 或 double+double?如果 int->float 包含相同的位,为什么要调用它进行提升?为什么不调用 float->int 进行促销?

如果您依赖隐式类型转换,您应该知道它们是如何工作的,否则只需手动转换。

有些语言可以设计成完全没有任何自动类型转换。并不是设计阶段的每一个决定都可以有充分的理由在逻辑上做出。

JavaScript 的鸭子类型在幕后有更模糊的决定。设计一种绝对逻辑的语言是不可能的,我认为这与哥德尔不完备定理有关。你必须平衡逻辑、直觉、实践和理想。

int在积分提升中提升为unsigned时,负值也会丢失(这导致0u < -1为真这样的乐趣)。

与 C 中的大多数机制(在 C++ 中继承)一样,通常的算术转换应该从硬件操作的角度来理解。 C 语言的创造者非常熟悉他们所使用的机器的汇编语言,他们编写 C 语言是为了让他们自己和像他们这样的人在编写之前一直用汇编语言编写的东西(例如 UNIX内核)。

现在,处理器通常没有混合类型的指令(将 float 添加到 double,将 int 与 float 进行比较等),因为这会极大地浪费晶圆上的空间——你必须实现与支持不同类型一样多的操作码。您只有 "add int to int," "compare float to float"、"multiply unsigned with unsigned" 等指令,因此首先需要进行通常的算术转换——它们是两种类型到指令族的映射,这使得大多数感觉与他们一起使用。

从习惯编写低级机器代码的人的角度来看,如果您有混合类型,在一般情况下您最有可能考虑的汇编指令是那些需要最少转换的指令。浮点数尤其如此,其中转换的运行时间很昂贵,尤其是在 70 年代初期,当时开发了 C,计算机速度很慢,而且浮点数计算是在软件中完成的。这显示在通常的算术转换中——只有一个操作数被转换(除了 long/unsigned int 的唯一例外,其中 long 可以转换为 unsigned long,这不需要在大多数机器上做任何事情。也许不需要在任何适用例外的地方)。

因此,编写通常的算术转换来执行汇编编码器大部分时间会执行的操作:您有两种不适合的类型,将一种转换为另一种,以便它适合。这就是你在汇编代码中所做的,除非你有特定的理由不这样做,并且对于习惯编写汇编代码并且 do 的人有特定的理由强制不同的转换,明确要求转换是自然的。毕竟,你可以简单地写

if((double) i < (double) f)

有趣的是,在这种情况下,顺便说一句,unsigned 在层次结构中比 int 更高,因此将 intunsigned 进行比较将以无符号比较结束(因此 0u < -1 位从一开始)。我怀疑这是一个指标,表明古人认为 unsigned 与其说是对 int 的限制,不如说是其值范围的扩展:我们现在不需要这个符号,所以让我们使用更大值范围的额外位。如果您有理由预计 int 会溢出,您就会使用它——在 16 位 int 的世界中,这是一个更大的担忧。

问题是为什么:因为它速度快,容易解释,容易编译,这些在当时开发C语言的时候都是非常重要的原因。

你可以有一个不同的规则:对于算术值的每次比较,结果是比较实际数值的结果。如果比较的表达式之一是常量,则在比较有符号和无符号 int 时需要一条附加指令,如果比较 long long 和 double 并在 long long 不能表示为 double 时想要正确的结果,那将是非常困难的。 (0u < -1 将是错误的,因为它会比较数值 0 和 -1 而不考虑它们的类型)。

在Swift中,通过禁止不同类型之间的操作,问题很容易解决。

有趣的是,这里的许多答案都从 C 语言的起源开始争论,明确指出 K&R 和历史包袱是 int 与 float 结合时转换为 float 的原因。

这是把责任推给了错误的一方。在 K&R C 中,没有浮动计算这样的东西。 所有 浮点运算均以双精度完成。出于这个原因,整数(或其他任何东西)永远不会隐式转换为浮点数,而只会转换为双精度数。 float 也不能​​是函数参数的类型:如果您真的、真的、真的想避免转换为 double,则必须将指针传递给 float。因此,函数

int x(float a)
{ ... }

int y(a)
float a;
{ ... }

有不同的调用约定。第一个得到一个 float 参数,第二个(现在不再允许作为语法)得到一个 double 参数。

单精度浮点运算和函数参数仅在 ANSI C 中引入。Kernighan/Ritchie 是无辜的。

现在有了新的单浮点数表达式(单浮点数以前只是一种存储格式),还必须进行新的类型转换。无论 ANSI C 团队在这里选择了什么(我会不知所措,没有更好的选择)都不是 K&R 的错。

can a float represent all int values?

对于 intfloat 都以 32 位存储的典型现代系统,没有。有些东西必须放弃。 32 位整数不会 1 对 1 映射到包含分数的相同大小的集合。

The i will be promoted to a float and the two float numbers will be compared…

不一定。您真的不知道适用什么精度。 C++14 §5/12:

The values of the floating operands and the results of floating expressions may be represented in greater precision and range than that required by the type; the types are not changed thereby.

虽然升级后i具有标称类型float,但该值可能会使用double硬件表示。 C++ 不保证浮点精度丢失或溢出。 (这在 C++14 中并不新鲜;它自古以来就是从 C 继承而来的。)

Why not promote both the int and the float to a double?

如果您想在任何地方都获得最佳精度,请改用 double,您永远不会看到 float。或 long double,但这可能 运行 更慢。考虑到一台机器可能提供多种替代精度,这些规则旨在对大多数有限精度类型的用例相对敏感。

大多数时候,快速和松散就足够了,所以机器可以自由地做最简单的事情。这可能意味着四舍五入的单精度比较,或双精度且无四舍五入。

但是,这样的规则最终是妥协,有时会失败。要在 C++(或 C)中精确指定算术,它有助于明确转换和提升。许多超可靠软件的风格指南完全禁止使用隐式转换,而且大多数编译器都会提供警告以帮助您删除它们。

要了解这些妥协是如何发生的,您可以仔细阅读 C rationale document。 (最新版本涵盖了 C99。)这不仅仅是 PDP-11 或 K&R 时代的无用包袱。

规则是为 16 位整数(最小要求大小)编写的。带有 32 位整数的编译器肯定会将两边都转换为双精度。无论如何,现代硬件中没有浮点寄存器,因此它 必须 转换为双精度。现在,如果您有 64 位整数,我不太确定它的作用。 long double 比较合适(通常是 80 位,但它甚至不是标准的)。