是否有任何值得注意的C扩展名包括其行为与机器字长无关的整数类型


12

与其他一些语言相比,C的一个有趣特征是它的许多数据类型基于目标体系结构的字长,而不是用绝对术语指定。虽然这允许该语言用于在某些类型可能有困难的机器上编写代码,但是这使得设计将在不同体系结构上一致运行的代码变得非常困难。考虑以下代码:

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

int16位(许多小型微控制器仍然适用)的体系结构上,此代码将使用定义良好的行为将值分配为1。在int64位机器上,它将再次使用定义良好的行为分配值4294836225。在int32位机器上,它可能会分配-131071的值(我不知道这是实现定义的还是未定义的行为)。即使代码只使用名义上称为“固定大小”类型的内容,该标准也要求当今使用的两种不同类型的编译器将产生两种不同的结果,而当今许多流行的编译器将产生三分之一。

这个特定的示例有些人为的,因为我不希望在现实世界的代码中将两个16位值的乘积直接分配给一个64位值,但是它被选择作为一个简短的示例来展示整数的三种方式促销可能与固定大小的无符号类型进行交互。在现实世界中,有必要根据数学整数算术规则对无符号类型的数学进行运算;在其他情况下,有必要根据模数运算的规则进行无符号类型运算;而在某些实际情况下,实际情况并非如此。没关系。诸如校验和之类的许多现实世界代码都依赖于uint32_t算术包装mod2³²,并且能够执行任意uint16_t 算术和获得的结果至少被定义为准确的mod 65536(与触发未定义行为相反)。

尽管这种情况显然似乎是不希望的(并且随着64位处理成为许多用途的规范,这种情况将变得越来越多),但据我所观察到的C标准委员会更喜欢引入已在某些著名产品中使用的语言功能。环境,而不是“从头开始”发明它们。C语言是否有任何显着扩展,这些扩展允许代码不仅指定如何存储类型,而且还指定在涉及可能晋升的场景中其应如何表现?我至少可以看到编译器扩展解决此类问题的三种方式:

  1. 通过添加一条指令,该指令将指示编译器将某些“基本”整数类型强制为一定大小。

  2. 通过添加一条指令,该指令将指示编译器评估各种升级方案,就像计算机的类型具有特定的大小一样,而与目标体系结构上类型的实际大小无关。

  3. 通过允许声明具有特定特征的类型(例如,声明一个类型应表现为mod-65536环绕的代数环,而不管其底层字长如何,并且不应隐式转换为其他类型;将a加到a wrap32int应产生a类型的结果(wrap32无论是否int大于16位),而将a wrap32直接添加到a wrap16应该是非法的(因为两个都不能转换为另一个)。

我自己的选择是第三个选择,因为它甚至可以使具有异常字长的机器也可以使用很多代码,这些代码希望变量像“二次幂”一样“自动换行”。编译器可能必须添加位屏蔽指令以使该类型具有适当的性能,但是如果代码需要包装mod 65536的类型,则最好让编译器在需要它的机器上生成此类屏蔽,而不是使源代码杂乱无章。或者在需要掩蔽的机器上根本无法使用此类代码。我很好奇,但是,是否有任何通用扩展可以通过上述任何一种方式,或者通过我没有想到的某种方式来实现可移植的行为。

为了澄清我在找什么,有几件事;最为显着地:

  1. 尽管可以通过多种方式来编写代码,以确保所需的语义(例如,定义宏以对特定大小的无符号操作数执行数学运算,以产生明确包装或不包装的结果),或至少防止不希望的情况语义(例如,有条件地定义一个类型wrap32_tuint32_t在编译器,其中一个uint32_t也不会得到提升,而这一数字是更好地为需要的代码wrap32_t到一些机器上该类型将得到提升,而不是有它运行并产生虚假行为失败编译)如果有任何一种编写代码的方式对将来的语言扩展最有利,那么使用该方式将比设计自己的方法更好。

  2. 对于如何扩展语言以解决许多整数大小的问题,我有一些很扎实的想法,允许代码在具有不同字长的机器上产生相同的语义,但是在我花大量时间编写它们之前,我想知道已经朝这个方向做了什么努力。

绝不希望我鄙视C标准委员会或他们所做的工作;但是,我希望在几年后,有必要使代码在“自然”升级类型为32位以及64位的计算机上正确工作。我认为,对语言进行一些适度的扩展(比C99 nnd C14之间的许多其他更改更适度),不仅可以提供一种有效使用64位体系结构的简洁方法,而且在讨价还价中还可以促进与该标准历来向后弯腰以支持“不寻常字长”的机器(例如,使具有12位的机器char可以运行期望uint32_t包装mod2³²]。根据未来扩展的方向,我还希望可以定义宏,这些宏将使今天编写的代码可在默认整数类型表现为“预期”的当今编译器上使用,也可在未来整数使用的编译器上使用类型将默认具有不同的行为,但是在哪里可以提供所需的行为。


4
@RobertHarvey您确定吗?据我了解,整数提升如果int大于uint16_t,则乘法的操作数将提升为int,并且乘法将作为int乘法进行,并且将结果int转换为int64_t,以初始化who_knows

3
@RobertHarvey怎么样?在OP的代码中,没有提到int,但仍会潜入。(再次假设我对C标准的理解是正确的。)

2
@RobertHarvey当然,这听起来很糟糕,但是除非您能指出这种方式,否则您不会说“不,您必须做错了什么”。真正的问题是如何避免整数提升,或者避免整数效应!

3
@RobertHarvey:C标准委员会的历史目标之一是使几乎任何机器都可以具有“ C编译器”,并且规则要足够具体,以使针对任何特定目标机器的独立开发的C编译器都能可以互换。人们在起草标准之前就开始为许多机器编写C编译器,并且标准委员会不想禁止编译器做现有代码可能依赖的任何事情,这使情况变得复杂。该标准的一些相当基本的方面...
supercat

3
……之所以如此,并不是因为有人试图制定一套“合理的”规则,而是因为委员会试图确定已经存在的独立编写的编译器具有的共同点。不幸的是,这种方法导致了一些标准,这些标准同时过于模糊,无法让程序员指定需要完成的工作,但是又过于具体,以至于不允许编译器“做到”。
超级猫

Answers:


4

作为这样的代码的典型意图

uint16_t ffff16 = 0xFFFF;
int64_t who_knows = ffff16 * ffff16;

是用64位(结果存储在变量中的大小)执行乘法,获得(与平台无关的)正确结果的通常方法是强制转换操作数之一以强制执行64位乘法:

uint16_t ffff16 = 0xFFFF;
int64_t i_know = (int64_t)ffff16 * ffff16;

我从未遇到过使该过程自动进行的C扩展。


1
我的问题不是如何对一个特定的算术表达式进行正确的求值(取决于一个运算符想要的结果类型,要么将操作数强制转换为,要么uint32_t使用定义为#define UMUL1616to16(x,y)((uint16_t)((uint16_t)(x)*(uint16_t)(y)))#define UMUL1616to16(x,y)((uint16_t)((uint32_t)(x)*(uint16_t)(y)))取决于的大小的宏int),而是是否存在关于如何有效地处理此类问题的任何新兴标准,而不是定义自己的宏。
2014年

我还应该提到,对于诸如哈希和校验和计算之类的事情,目的通常是获取结果并将其截断为操作数的大小。像(ushort1*ushort2) & 65535u这样的表达式的典型意图是对所有操作数值执行mod-65536算术运算。阅读C89的基本原理后,我认为很明显,尽管作者认识到如果结果超过2147483647,则此类代码可能在某些实现中会失败,但他们希望此类实现会越来越少。但是,此类代码有时在现代gcc上失败。
超级猫
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.