29

I've read cppreference.com's implicit conversion:

Integral promotion:
prvalues of small integral types (such as char) may be converted to prvalues of larger integral types (such as int).

[...]

Note that all other conversions are not promotions; for example, overload resolution chooses char -> int (promotion) over char -> short (conversion).

The conversion from char to int is 'promotion'; it's clear (from one byte to 4 bytes).
The conversion from char -> short is 'not promotion'; why?
I've been thinking that char is one byte, and short (short int) is two bytes. Why isn't it considered a promotion? It seems to contradict the first line. Doesn't it mean 'convert from small type to bigger type' is promotion?)


After looking for some answers:

Can we consider "a promotion is a special case of a conversion"? Can we say "all promotions are conversions, but not all conversions are promotions"?
Or should we consider them as two different and separate concepts?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
LiDa Cute
  • 357
  • 2
  • 7
  • 4
    I think it's just a terminology quirk. Promotion comes as heritage from C language, by definition it converts a smaller type to `int`. Conversion is anything else that can be done implicitly (e.g. char -> short, or int -> bool, or like when you define some custom conversion operator to bool, etc) – Alexey S. Larionov Aug 24 '23 at 12:52
  • 3
    Don't get too tied up with the *actual* sizes of various integral types. Even if `short` and `int` have the same bit-width (which is allowed), they have different **ranks**. And only the basic `int` type is used in 'automatic' promotions. – Adrian Mole Aug 24 '23 at 12:54
  • Well, every C++ theory is a mystery to me. I seem to never get out a lesson without being confused – LiDa Cute Aug 24 '23 at 12:57
  • 7
    Historically, `int` is special, being the "native" integral type (e.g. with all operations implemented with dedicated hardware instructions) so more efficient to convert smaller integral types (like `short` and `char`) to `int` and doing operations rather than using instructions directly on the smaller types. This history is (part of) why integral types have ranks in C and C++, and why "promotions" to `int` are distinguished other integral conversions. The distinction is less important (or unimportant!) with modern processors, but supports compatibility with systems where it still matters. – Peter Aug 24 '23 at 13:30
  • Those specific sizes for `short` and `int` are your _implementation-defined_ values. – Toby Speight Aug 25 '23 at 08:04
  • 1
    I think the history of promotions goes right back to origins of C as a development of the B language, which had no types, and represented everything as a _machine word_. B's integer type became C's `int`. – Toby Speight Aug 25 '23 at 08:07
  • @AlexeyS.Larionov Yes, it is terminology, but not *only* terminology: The integral *promotions* (but not the conversions -- because: which one?) are sometimes mandated by the language. For example, they are part of the *default argument promotions* of function arguments which do not have a corresponding parameter (and are accessed by the caller via v_arg). A prominent example is `printf`. You cannot pass a `char` or short to `printf`, `printf`s implementation always sees an `int` (but of course you can supply a conversion specification which influences the interpretation of that `int` value). – Peter - Reinstate Monica Aug 25 '23 at 14:07
  • @AlexeyS.Larionov Even more commonly, integral promotion is performed for lower-ranked-than-int arguments to arithmetic operators. I.e., the expression `(unsigned char)255 + (unsigned char)1` has type `int` and value 256. – Peter - Reinstate Monica Aug 25 '23 at 14:18

4 Answers4

34

Historical motivation: C

The idea of integral promotions dates all the way back to pre-standard C. When providing arguments to variadic functions (...) or to functions without a prototype, promotions are applied. I.e. calling:

// 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) { /* ... */ }

Promotions are basically a "minimal" upgrade to the type, whereas conversions can be anything. You could even argue that the design is a consequence of building on the B programming language, which didn't even have multiple integer types like C does.

Relevant wording in the C++ Standard

Only the following are considered integer promotions:

A prvalue that is not a converted bit-field and has an integer type other than bool, char8_t, char16_t, char32_t, or wchar_t whose integer conversion rank is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int.

- [conv.prom] p2

The difference between promotions and conversions is explained here:

The conversions allowed as integral promotions are excluded from the set of integral conversions.

- [conv.integral] p4

As you can see, there is some overlap between the two concepts, but any conversion which would also be a promotion is not considered a conversion.

Whether a standard conversion is a promotion or a conversion has an impact on overload resolution:

Standard conversion sequences are ordered by their ranks: an Exact Match is a better conversion than a Promotion, which is a better conversion than a Conversion. [...]

- [over.ics.rank] p4

Impact on overload resolution

As you've pointed out, char -> short is a conversion, and char -> int is a promotion. This has the following impact:

// 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');
}

If C++ was designed from scratch nowadays, promotions and conversions would likely be defined a lot differently, however, the status quo is what we have, and it's unlikely to change. Changing this behavior and wording after all these years is basically impossible because of how much code depends on it.

As with so many design decisions in C++, the answer is: historical reasons.

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • 1
    I expect to see your last code snippet on twitter in a few days. Holy hell, C++ never ceases to surprise me. – julaine Aug 24 '23 at 13:05
  • @Jan Schultke: Can we consider "a promotion is a special case of a conversion"? , can we say "all promotions are conversions, but not all conversions are promotions"? Or should we consider them as 2 different and separate concepts? – LiDa Cute Aug 24 '23 at 13:16
  • 2
    Hmm. I'm not sure, myself, but is an *explicit* conversion (i.e. with a cast) from a small integral type to an `int` still classified as *promotion*? Or is promotion only when the compiler 'does it itself'? – Adrian Mole Aug 24 '23 at 13:17
  • 2
    @LiDaCute good question. I've updated the answer to explain that. – Jan Schultke Aug 24 '23 at 13:30
  • @AdrianMole when you explicitly convert, such as through `static_cast`, promotions are applied first, then conversions. You can find the details here: https://eel.is/c++draft/expr.static.cast – Jan Schultke Aug 24 '23 at 13:35
  • @JanSchultke I never know the standard did exclude promotions from the set of integral conversions if you did not help me to point it out. thanks :> – LiDa Cute Aug 24 '23 at 13:43
  • 4
    Promotions are also done for operands to most math operators. A notorious example is that `unsigned short x = 65535; unsigned tmp = x*x;` has signed-overflow UB on a machine with 16-bit `short` and 32-bit `int`, because the integer promotion from `short` produces *signed* `int` = 65535. And squaring that produces `0xfffe0001`. It doesn't promote to `unsigned int` because signed `int` covers the full value-range of `uint16_t`. And it doesn't stay as `unsigned short` because its conversion rank is less than `int`. – Peter Cordes Aug 25 '23 at 10:16
  • 2
    @PeterCordes: Further, according to the published C99 Rationale, the decision to have `unsigned short` promote to `int` was motivated by the fact that quiet wraparound two's-complement implementations (which were then, and were presumably expected to remain, the majority of current implementations) would process handle the cases of `unsigned tmp=ushort1*ushort2;` where the mathematical product would exceed INT_MAX in a fashion that would wrap the result negative, but then wrap that back around to yield the arithmetically-correct result. Compilers like gcc no longer do that, however. – supercat Aug 25 '23 at 16:25
  • 1
    @JanSchultke I edited your excellent answer to add also the standard reference for the ranking difference (which you show in your example). Feel free to revert the change if you think it adds noise. – dfrib Aug 28 '23 at 09:07
15

Both char to int and char to short are conversions. Note the text’s use of “all other conversions” after discussing promotions: Promotions are a type of conversion.

A conversion is an operation that takes a value in one form and produces the same value (as nearly as possible) in another form:

  • Taking a char value and producing an int with the same value is a conversion.
  • Taking an int value and producing a string containing a decimal numeral representing the same value is a conversion.
  • Taking a weight in pounds and producing the same weight in kilograms is a conversion.

In many places in expressions, C++ automatically converts narrow integer types to wider integer types. This is done largely for historical purposes, and also because it could be awkward to do arithmetic purely in narrow types—char arithmetic would overflow too easily, and requiring the programmer to explicitly insert casts to int would make code cumbersome.

These special conversions are called “promotions.” The natural English meaning of the word, advancing to a higher position, fits these types of conversions: They all convert from a narrower type to a wider type (or to one at least as wide). Aside from that, the word is adopted to particularly designate these special conversions. An important feature of the integral promotions is they never change the value. (Ideally, no conversion would change a value, but this does happen due to constraints. One example is that converting a float value of 3¼ to int must produce an integer, so 3 is produced instead of 3¼. Another is that converting an int value of −3 to unsigned forces wrapping, so a value that is very different nominally is produced, although it does have a special relationship with the input value and can be considered the same value in modulo arithmetic.)

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
5

The C++ standard specifies a set of rules for integral promotions, and these rules are not solely based on the size of the types. The decision to promote a char to an int rather than a short is based on the historical design of the language and the desire for consistent behavior across different platforms.

According to the C++ standard, integral promotions are defined to convert small integral types to a particular implementation-defined type, which is usually int. Since int is chosen as the target type for integral promotion, converting a char to a short doesn't qualify as a promotion, even though short is larger than char. It falls under the category of conversions instead.

The specific rules can vary between different implementations and architectures, but this is the general reasoning behind the behavior you observed. It's a part of the language's design, guided by historical considerations and practical aspects of compiler implementation, rather than a strictly logical progression based on type size.

NoName
  • 643
  • 3
  • 8
  • IIRC, it is possible for a `char` to be an **unsigned** 8-bit type (`char` is guaranteed to be one byte) and also a `short` to be 8-bits (I think). Then, conversion between unsigned and signed types of the same size could result in an erroneous destination value. However, automatic integral **promotion** guarantees that the result is the same value as the original. (Note that this may be nonsense: The C++ Standard may dictate that `short int` must be at least 16 bits ... need to check!) – Adrian Mole Aug 24 '23 at 13:03
  • 5
    @AdrianMole: , `short` indeed has a minimal range that requires 16 bits. But `char` can be 16 bits too. `sizeof` returns a multiple of `CHAR_BITS`. So `sizeof(short)==1` is still allowed. – MSalters Aug 24 '23 at 13:09
  • @MSalters: I think one of the reasons `char` needs to be able to be signed (beyond compatibility with existing code) is that every value of `char` is supposed to fit within an `int`. On a platform where `sizeof(int)==1`, that would imply that `char` must be signed. Additionally, on 8-bit platforms using EBCDIC, a `signed char` would be unable to hold represent the character codes associated with digits 0-9 (i.e. 0xF0 to 0xF9) as positive numbers, implying that on those machines `char` would need to be unsigned. – supercat Aug 25 '23 at 16:33
2

The promotions are a subset of conversions performed implicitly in certain circumstances, even if the target type is not given. Here are two of them:

  • Many built-in arithmetic operators "promote" their arguments to int if they are of lesser rank. The following program prints 256, because both chars are promoted to int before they are added, and the resulting expression has type int; by contrast, the second printf prints 0, because ++ does not promote its argument so that the 8 bit value overflows (which is defined for unsigned types).
#include <cstdio>
int main()
{
    unsigned char c1 = 255, c2 = 1;
    std::printf("%d\n", c1 + c2);
    std::printf("%d\n", ++c1);
}
  • Talking about printf: Integral arguments of lesser rank than int which do not have a corresponding parameter in the function declaration (like the second arguments in the printf calls above) are promoted to int. It is impossible to pass genuine chars to printf. (Note that in the second printf above, the expression ++c1 has type unsigned char, with the value 0, which is promoted to int when the argument is passed, but after the evaluation of the expression.)

Other widening conversions which are not promotions, like in unsigned short c = 'a';, are performed because the target of an assignment, the type of a formal parameter or an explicit conversion demand it.

Peter - Reinstate Monica
  • 15,048
  • 4
  • 37
  • 62