为什么'char -> int'是提升,而'char -> short'是转换(但不是提升)?

42
我已阅读cppreference.com的隐式转换:
整数提升: 小整数类型(如char)的prvalue可以转换为大整数类型(如int)的prvalue。
[...] 请注意,所有其他转换都不是提升;例如,重载解析会选择char -> int(提升),而不是char -> short(转换)。
从char到int的转换是“提升”;它很明显(从1个字节到4个字节)。 从char到short的转换不是“提升”;为什么? 我一直认为char是一个字节,而short(short int)是两个字节。为什么它不被视为提升?这似乎与第一行相矛盾。难道它不是指“从小类型转换到较大类型”就是提升吗?

寻找一些答案后:

我们能否认为"晋升是转化的特例"?我们可以说"所有的晋升都是转化,但并非所有的转化都是晋升"吗?
还是应该将它们看作是两个不同且独立的概念?


5
我认为这只是一个术语上的小问题。"Promotion"这个词是从C语言继承而来的,根据定义它将较小的类型转换为int。而"Conversion"则指的是其他可以隐式完成的转换(例如char -> short,或int -> bool,或者当你定义一些自定义的转换运算符到bool等等)。 - Alexey S. Larionov
4
不要过于纠结于各种整型的实际大小。即使shortint有相同的位宽(这是允许的),它们有不同的等级。而且只有基本的int类型在"自动"提升中使用。 - Adrian Mole
1
嗯,每一项C++理论对我来说都是个谜。我似乎从未在课堂上不感到困惑就离开过。 - LiDa Cute
9
历史上,int 是特殊的,它是“本地”的整数类型(例如,所有操作都通过专用硬件指令实现),因此将较小的整数类型(如 shortchar)转换为 int 并进行操作比直接在较小的类型上使用指令更高效。这段历史是 C 和 C++ 中整数类型具有等级以及 "升级" 为 int 不同于其他整数转换的原因之一。这种区别在现代处理器中不太重要(或无关紧要!),但支持与仍然相关的系统的兼容性。 - Peter
1
我认为促销的历史可以追溯到 C 语言作为 B 语言的发展,B 语言没有类型,将所有内容都表示为“机器字”。B 的整数类型成为了 C 的 int - Toby Speight
显示剩余3条评论
4个回答

40

历史动机:C

整数提升的概念可以追溯到标准化之前的C语言时期。当向可变参数函数(...)或没有原型的函数传递参数时,会进行提升操作。例如:

// function declaration with no prototype
void mystery();
// ...
char c = 'c';
mystery(c); // this promotes c to int

// in another .c file, someone could define
void mystery(int x) { /* ... */ }

促销基本上是对类型的“最小”升级,而转换可以是任何形式。 你甚至可以说设计是建立在B编程语言的基础上的结果,而B编程语言甚至没有像C语言那样具有多个整数类型。
C++标准中相关的措辞如下:
只有以下情况被视为整数提升:
一个不是转换后的位域,并且具有除了bool、char8_t、char16_t、char32_t或wchar_t之外的整数类型的prvalue,其整数转换等级低于int的等级,如果int能够表示源类型的所有值,则可以将其转换为int类型的prvalue;否则,源prvalue可以转换为unsigned int类型的prvalue。
- [conv.prom] p2 这里解释了促销和转换之间的区别:
作为整数提升允许的转换被排除在整数转换集合之外。
- [conv.integral] p4

如你所见,这两个概念之间存在一些重叠,但任何既是转化又是提升的转换都不被视为转化。

标准转换是提升还是转化对过载解析有影响:

标准转换序列按其级别排序:精确匹配比提升更好,提升比转化更好。[...]

- [over.ics.rank] p4

对过载解析的影响

正如你所指出的,char -> short 是一个转换,而 char -> int 是一个提升。 这会产生以下影响:

// A conversion sequence from char -> int is empty
// because char -> int is a promotion, and so it doesn't contribute
// to the conversion sequence.
void f(int);
// A conversion sequence from char -> short has length 1
// because char -> short is not a promotion.
void f(short);

int main() {
    // Not an ambiguous call; calls f(int), because the conversion sequence
    // for this call is shorter.
    // Note that in C, a character literal has type 'int', not 'char', so
    // it is more symmetrical to favor 'int' whenever possible.
    f('x');
}

如果现在从头开始设计C++,提升和转换的定义可能会有很大不同,但是现状就是我们拥有的,而且不太可能改变。 由于这么多年来依赖它的代码量非常庞大,要改变这种行为和措辞基本上是不可能的。
与C++中的许多设计决策一样,答案是:历史原因。

1
我期待在几天后在Twitter上看到你的最新代码片段。天哪,C++真是让我始终感到惊喜。 - julaine
3
嗯。我不确定,但是将小整数类型向int进行显式转换(即使用强制类型转换)仍然被归类为提升吗?还是只有当编译器'自己执行'时才算是提升? - Adrian Mole
3
@LiDaCute 很好的问题。我已经更新了答案来解释这个问题。 - Jan Schultke
5
对于大多数数学运算符,操作数也会进行提升。一个臭名昭著的例子是,在具有16位short和32位int的机器上,unsigned short x = 65535; unsigned tmp = x*x;会产生有符号溢出UB,因为从shortint的整数提升会产生有符号int = 65535。然后对其进行平方运算会得到0xfffe0001。它不会提升为unsigned int,因为有符号int覆盖了uint16_t的全部值范围。它也不会保持为unsigned short,因为其转换等级低于int - Peter Cordes
3
@PeterCordes:根据已发布的C99 Rationale,将unsigned short提升为int的决定是基于这样一个事实:安静地环绕的二进制补码实现(当时并且预计仍然是大多数当前实现)可以处理unsigned tmp=ushort1*ushort2;这种情况,其中数学乘积会超过INT_MAX,并以一种使结果变为负数的方式进行包装,但然后再将其包装回来以产生算术上正确的结果。然而,像gcc这样的编译器不再这样做了。 - supercat
显示剩余4条评论

16

char转换为int和将char转换为short都是转换操作。注意在讨论提升后,文本使用了“所有其他转换”的用法:提升是一种转换。

转换是一种将一个形式的值转换为另一种形式的操作(尽可能地保持相同的值):

  • char值转换为具有相同值的int是一种转换。
  • int值转换为包含表示相同值的十进制数的字符串是一种转换。
  • 将重量从磅转换为千克是一种转换。

在表达式中的许多位置,C++会自动将窄整数类型转换为宽整数类型。这主要是出于历史目的,并且也因为纯粹使用窄类型进行算术运算可能会很麻烦——char算术运算会很容易溢出,而要求程序员显式插入int的强制转换会使代码冗长。

这些特殊的转换被称为“提升”。这个词在自然英语中的意思是晋升到更高的职位,与这些类型的转换相符:它们都从较窄的类型转换为更宽的类型(或至少与之一样宽)。除此之外,这个词还被用来特别指代这些特殊的转换。整数提升的一个重要特点是它们永远不会改变值。(理想情况下,没有任何转换会改变值,但由于限制条件,这种情况确实会发生。一个例子是将浮点数值3¼转换为整数必须产生一个整数,所以产生的结果是3而不是3¼。另一个例子是将整数值-3转换为无符号数会强制进行包装,因此会产生一个在名义上非常不同的值,尽管它与输入值有着特殊的关系,并且可以在模运算中被视为相同的值。)

6
C++标准规定了一组关于整数提升的规则,这些规则不仅仅基于类型的大小。将一个char提升为int而不是short的决定是基于语言的历史设计和在不同平台上保持一致行为的愿望。
根据C++标准,整数提升被定义为将小整数类型转换为特定的实现定义类型,通常是int。由于int被选择为整数提升的目标类型,将char转换为short并不符合提升的条件,即使short比char大。它属于转换的范畴。
具体规则可能因不同的实现和架构而有所不同,但这就是你观察到的行为背后的一般原理。这是语言设计的一部分,受历史考虑和编译器实现的实际方面的指导,而不是严格基于类型大小的逻辑进展。

据我所知,char 可能是一个无符号的8位类型(char 保证为一个字节),而 short 可能是8位(我想)。然后,在相同大小的有符号和无符号类型之间进行转换可能会导致错误的目标值。然而,自动整数提升保证结果与原始值相同。(请注意,这可能是无意义的:C++ 标准可能规定 short int 必须至少为16位...需要检查!) - Adrian Mole
6
@AdrianMole:short的确有一个需要16位的最小范围。但是char也可以是16位的。sizeof返回CHAR_BITS的倍数。所以sizeof(short)==1仍然是允许的。 - MSalters
@MSalters:我认为 char 需要能够被标记为有符号的一个原因(除了与现有代码的兼容性),是因为 char 的每个值都应该适应于 int 中。在 sizeof(int)==1 的平台上,这意味着 char 必须是有符号的。此外,在使用 EBCDIC 的 8 位平台上,signed char 将无法表示与数字 0-9 相对应的字符编码(即 0xF0 到 0xF9)作为正数,这意味着在这些机器上 char 需要是无符号的。 - supercat

3
促销是在特定情况下隐式执行的转换的一个子集,即使没有给出目标类型。以下是其中两个例子:
- 许多内置算术运算符会将其参数“提升”为`int`,如果它们的等级较低。下面的程序打印256,因为两个`char`在相加之前都被提升为`int`,并且结果表达式的类型是`int`;相比之下,第二个`printf`打印0,因为`++`不会提升其参数,所以8位值会溢出(这对于无符号类型是定义好的)。
#include <cstdio>
int main()
{
    unsigned char c1 = 255, c2 = 1;
    std::printf("%d\n", c1 + c2);
    std::printf("%d\n", ++c1);
}

谈到printf:在函数声明中没有相应参数的低于int级别的整数参数(如上面printf调用中的第二个参数)会被提升为int。无法将真正的char传递给printf。(请注意,在上面的第二个printf中,表达式++c1的类型为unsigned char,值为0,在传递参数时被提升为int,但在表达式求值之后。)
其他不是提升的扩展转换,比如unsigned short c = 'a';,是因为赋值的目标、形式参数的类型或显式转换的需求而执行的。

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