11

The C and C++ standards stipulate that, in binary operations between a signed and an unsigned integer of the same rank, the signed integer is cast to unsigned. There are many questions on SO caused by this... let's call it strange behavior: unsigned to signed conversion, C++ Implicit Conversion (Signed + Unsigned), A warning - comparison between signed and unsigned integer expressions, % (mod) with mixed signedness, etc.

But none of these give any reasons as to why the standard goes this way, rather than casting towards signed ints. I did find a self-proclaimed guru who says it's the obvious right thing to do, but he doesn't give a reasoning either: http://embeddedgurus.com/stack-overflow/2009/08/a-tutorial-on-signed-and-unsigned-integers/.

Looking through my own code, wherever I combine signed and unsigned integers, I always need to cast from unsigned to signed. There are places where it doesn't matter, but I haven't found a single example of code where it makes sense to cast the signed integer to unsigned.

What are cases where casting to unsigned in the correct thing to do? Why is the standard the way it is?

Community
  • 1
  • 1
Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • 7
    Pretend its the 1970s - think Disco. Signed comes in 3 varieties, 2's complement, 1's complement and signed magnitude. Unsigned comes in only 1 variety. Rules about mixed types are bad enough. Yet by going to signed type, the result becomes more complex as the rules need 3 variants destinations rather than 1 when going to unsigned. – chux - Reinstate Monica Apr 11 '17 at 03:29
  • 1
    @chux, I think the reasoning might be related to that. But why would you need three rules? If casting -1 to unsigned can be done in a single, well-defined way across the 3 varieties of signed ints, why can't casting `0xFFFF` to unsigned be done in a single, well-defined way? (Note there I'm thinking of 16-bit ints, since it's the 70's and all.) – Cris Luengo Apr 11 '17 at 03:37
  • 2
    The C language - where this rule was invented, as others have said - there were only a few data structures - naked arrays, structs, and pointers - and that matched the simple machine addressing modes at the time - things like indexed, indirect, base+displacement, base+displacement+index. Now .. it is true that the index addressing modes in standard machines at the time would take signed integers ... but it was also true that the programmers of the time didn't often use negative numbers to index ... and they _did_ need the 2x range of unsigned on their 16-bit machines (esp. in fields of words) – davidbak Apr 11 '17 at 03:40
  • 1
    Chris: The standard only guarantees that an unsigned value has as many value bits as a signed value of the same rank, not that the sign bit is reinterpreted as a value bit. So you could handle a non-2s-complement architecture by forcing the sign bit to positive for unsigned values, thus restricting the range of representable values to the range of representable positive values in the signed type. This makes both conversion s simple (unsigned to signed becomes a no-op or a mask). But it also means that the signed type will always be chosen for the standard conversion. – rici Apr 11 '17 at 04:15
  • @rici, if you define the unsigned int that way, you would end up preferring the signed int over the unsigned int for all operations. – Cris Luengo Apr 11 '17 at 04:41
  • @davidbak, yes, the extra bit makes a lot of difference if you have so few of them. That's a good reason for unsigned integers to exist. And given that you easily use the full range, conversion to a signed int is likely to overflow. That could be a good reason to prefer the unsigned int. But I'm not sure people in the 70's used negative offsets less often than nowadays. – Cris Luengo Apr 11 '17 at 04:45
  • 1
    @chris: yes, that's what I meant by the last sentence of my comment. And that's unfortunate because `ua+ub` might overflow as UB even though both variables have type `unsigned`. You'd need to do arithmetic as`unsigned long` to avoid that. Fortunately, non-2s-complement architectures are rare (if they even exist). – rici Apr 11 '17 at 04:52
  • `let's call it strange behavior` Hmm ... why do you find it strange? – Support Ukraine Apr 11 '17 at 05:57
  • 2
    "I always need to cast from unsigned to signed. " - bear in mind that this is non-portable when the unsigned has a value greater than the maximum of the signed type . The compiler warns for a reason; by casting here you are saying "Oh that will never happen" – M.M Apr 11 '17 at 06:19
  • I read your question like: Why are `signed to unsigned` conversion preferred over `unsigned to signed` conversion? If that's your question the answer is simple: The first is well-defined while the second is implementation-defined. – Support Ukraine Apr 11 '17 at 06:45
  • @4386427, I think it's strange behavior because (as I wrote in the question) in every case I have seen of combining signed and unsigned, it's the wrong choice. As to your answer: there's an answer below that suggests the same, but the conversion is only well-defined because the standard chose to do so. They could have chosen to define the other conversion, and make that one the "default". – Cris Luengo Apr 11 '17 at 14:35
  • @M.M, Non-portable is not so important. What matters is that the number could change. Whether it changes in an implementation-defined way or in a standard-defined way is not so important to me, unless you're interested in modulo arithmetic. You need to make sure the conversion is OK by checking limits, and you need to do that with conversions both ways. – Cris Luengo Apr 11 '17 at 14:38
  • @CrisLuengo You commented: `the conversion is only well-defined because the standard chose to do so. They could have chosen to define the other conversion, and make that one the "default". ` Seems you missing the history of signed number representation. When C came to life and later standardized there were different ways of representing signed numbers. They differ in such a way that signed to unsigned conversion can't be defined to behave the same for all signed representations. Further, you commented `Non-portable is not so important` Well, maybe you don't care but many others do care. – Support Ukraine Apr 12 '17 at 05:36
  • @4386427 I don't buy that line of reasoning. The standard gives as conversion: `uint = sint < 0 ? uint_max - sint : sint`. They could have defined the inverse conversion as `sint = uint > sint_max ? uint - uint_max : uint`. The operations involved are similar in complexity. The signed representation has nothing to do with the standard. Note that the standard followed the two's complement bit-reinterpretation cast. Architectures that use a different representation need to do some computation. This is again true for conversions both ways. – Cris Luengo Apr 12 '17 at 14:03
  • @4386427 (cont) The fact is, the standard chose an implicit conversion for mixed arithmetic, and then had to define that conversion. This is why signed to unsigned is well defined. Re portability: if you don't change the value, the conversion is well defined. If you change the value, usually you're in trouble. Why is it important to have the same trouble on different platforms? – Cris Luengo Apr 12 '17 at 14:09
  • @Cris: The standard conversion from signed to unsigned is a no-op on 2s complement machines. Your proposed inverse is a no-op on 1s-complement machines; on 2s-complement machines, it artificially reduces the range of uniquely convertible signed values. So they are not symmetric, and given the huge preponderance of 2s complement machines, the conversion chosen by the standard is the natural choice. – rici Apr 12 '17 at 20:26

4 Answers4

12

Casting from unsigned to signed results in implementation-defined behaviour if the value cannot be represented. Casting from signed to unsigned is always modulo two to the power of the unsigned's bitsize, so it is always well-defined.

The standard conversion is to the signed type if every possible unsigned value is representable in the signed type. Otherwise, the unsigned type is chosen. This guarantees that the conversion is always well-defined.


Notes

  1. As indicated in comments, the conversion algorithm for C++ was inherited from C to maintain compatibility, which is technically the reason it is so in C++.

  2. When this note was written, the C++ standard allowed three binary representations, including sign-magnitude and ones' complement. That's no longer the case, and there's every reason to believe that it won't be the case for C either in the reasonably bear future. I'm leaving the footnote as a historical relic, but it says nothing relevant to the current language.

    It has been suggested that the decision in the standard to define signed to unsigned conversions and not unsigned to signed conversion is somehow arbitrary, and that the other possible decision would be symmetric. However, the possible conversion are not symmetric.

    In both of the non-2's-complement representations contemplated by the standard, an n-bit signed representation can represent only 2n−1 values, whereas an n-bit unsigned representation can represent 2n values. Consequently, a signed-to-unsigned conversion is lossless and can be reversed (although one unsigned value can never be produced). The unsigned-to-signed conversion, on the other hand, must collapse two different unsigned values onto the same signed result.

    In a comment, the formula sint = uint > sint_max ? uint - uint_max : uint is proposed. This coalesces the values uint_max and 0; both are mapped to 0. That's a little weird even for non-2s-complement representations, but for 2's-complement it's unnecessary and, worse, it requires the compiler to emit code to laboriously compute this unnecessary conflation. By contrast the standard's signed-to-unsigned conversion is lossless and in the common case (2's-complement architectures) it is a no-op.

rici
  • 234,347
  • 28
  • 237
  • 341
  • 2
    *"Casting from unsigned to signed results in the undefined behaviour if the value cannot be represented."* Nope. – Baum mit Augen Apr 11 '17 at 03:31
  • But the one cast is defined and the other isn't because it's written like that in the standard. They could have defined the cast to signed int, if they'd wanted, or? (btw: downvote not mine) – Cris Luengo Apr 11 '17 at 03:33
  • Keeping the -1, as this is certainly not why *the C++* standards used that definition. – Baum mit Augen Apr 11 '17 at 03:52
  • 1
    @chris: the restriction comes frim the assumption that some architectures will trap overflow, and the standard has traditionally avoided specifications which would impose extra checks to avoid traps. – rici Apr 11 '17 at 03:52
  • 1
    *"The standard conversion is to the signed type if every possible unsigned value is representable in the signed type. Otherwise, the unsigned type is chosen. This guarantees that the conversion is always well-defined."* Also, that reasoning is nonsense. When writing a standard, you could make the opposite result well-defined just as well. – Baum mit Augen Apr 11 '17 at 04:00
  • @rici, the overflow is an interesting point. Unsigned to signed could overflow, but signed to unsigned will not. Are underflow traps less common? – Cris Luengo Apr 11 '17 at 04:38
  • @BaummitAugen, I agree with you that the argument of "because it guarantees a well-defined result" is not correct, as it would have been possible to write the standard such that any other choice would have lead to a well-defined result. But your very first comment to this answer is not right: conversion from unsigned to signed can lead to overflow, and so the result is architecture and compiler-dependent (maybe not UB as in "bad things will happen", but undefined behavior as in not defined what will happen. – Cris Luengo Apr 11 '17 at 04:55
  • @chris I don't believe overflow traps are common enough that it is possible to meaningfully compare. But if a machine supports unsigned arithmetic, it will not trap when you attempt to use a signed value as unsigned. On the other hand, the unsigned value could conceivably turn into a trapping negative zero, if such a thing exists and if unsigned values use the sign bit. – rici Apr 11 '17 at 05:12
  • the comments about "casting" also apply to (implicit) conversions – M.M Apr 11 '17 at 06:17
  • @rici In 2s complement, -0 is not a different value for integers, and C++ (now) mandates 2s complement. But I see now you said "non-2s complement", my bad; I misread it as "2s complement and not" basically. – Yakk - Adam Nevraumont Jun 21 '21 at 02:17
3

If the signed casting was chosen, then simple a+1 would always result in signed type (unless constant was typed as 1U).

Assume a was unsigned int, then this seemingly innocent increment a+1 could lead to things like undefined overflow or "index out of bound", in the case of arr[a+1]

Thus, "unsigned casting" seems like a safer approach because people probably don't even expect casting to be happening in the first place, when simply adding a constant.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
Radzor
  • 162
  • 1
  • 7
  • Interesting thought. Of course, `a` would first be cast to `int` if it was an 8-bit integer, but if `a` is `unsigned int`, then it certainly makes sense. – Cris Luengo Sep 12 '20 at 06:36
  • I am going to change it to unsigned int. I forgot for a second about integer promotion. – Radzor Sep 13 '20 at 16:56
1

This is sort of a half-answer, because I don't really understand the committee's reasoning.

From the C90 committee's rationale document: https://www.lysator.liu.se/c/rat/c2.html#3-2-1-1

Since the publication of K&R, a serious divergence has occurred among implementations of C in the evolution of integral promotion rules. Implementations fall into two major camps, which may be characterized as unsigned preserving and value preserving. The difference between these approaches centers on the treatment of unsigned char and unsigned short, when widened by the integral promotions, but the decision has an impact on the typing of constants as well (see §3.1.3.2).

... and apparently also on the conversions done to match the two operands for any operator. It continues:

Both schemes give the same answer in the vast majority of cases, and both give the same effective result in even more cases in implementations with twos-complement arithmetic and quiet wraparound on signed overflow --- that is, in most current implementations.

It then specifies a case where ambiguity of interpretation arises, and states:

The result must be dubbed questionably signed, since a case can be made for either the signed or unsigned interpretation. Exactly the same ambiguity arises whenever an unsigned int confronts a signed int across an operator, and the signed int has a negative value. (Neither scheme does any better, or any worse, in resolving the ambiguity of this confrontation.) Suddenly, the negative signed int becomes a very large unsigned int, which may be surprising --- or it may be exactly what is desired by a knowledgable programmer. Of course, all of these ambiguities can be avoided by a judicious use of casts.

and:

The unsigned preserving rules greatly increase the number of situations where unsigned int confronts signed int to yield a questionably signed result, whereas the value preserving rules minimize such confrontations. Thus, the value preserving rules were considered to be safer for the novice, or unwary, programmer. After much discussion, the Committee decided in favor of value preserving rules, despite the fact that the UNIX C compilers had evolved in the direction of unsigned preserving.

Thus, they consider the case of int + unsigned an unwanted situation, and chose conversion rules for char and short that yield as few of those situations as possible, even though most compilers at the time followed a different approach. If I understand right, this choice then forced them to follow the current choice of int + unsigned yielding an unsigned operation.

I still find all of this truly bizarre.

Cris Luengo
  • 55,762
  • 10
  • 62
  • 120
  • Also note that this committee had the really difficult task of merging all these different implementations of K&R C that were going around on all these very different platforms. They worked on the draft between 1983 and 1989. That's a long time! – Cris Luengo Apr 12 '17 at 18:06
  • 2
    Cris: That reasoning is with respect to a different issue. I was going to include it in my answer but you asked specifically about the conversion between signed and unsigned `int`, whereas the debate you quote is about the promotion of unsigned types narrower than `int`. This didn't arise in K&R C because K&R C didn't have unsigned integers other than `unsigned int` (and bitfields, but that's another kettle of fish). The committee didn't actually care much about `signed + unsigned`; the problematic case is `signed < unsigned` (also division, but in practice that's rare). – rici Apr 12 '17 at 19:21
  • @rici: Do you know why the committee didn't care much about that? I still haven't seen an example where conversion to unsigned is better than conversion to signed. This bit from the rationale that I found says that "Exactly the same ambiguity arises whenever an `unsigned int` confronts a `signed int` across an operator, and the signed int has a negative value." I take that to mean that this is a related discussion. But you are right that this comes from the integer promotion section. – Cris Luengo Apr 12 '17 at 19:28
  • 1
    because most implementations are/were 2s-complement with implicitly modular arithmetic. So in the common case, there was no difference in observed behaviour with arithmetic operators other than division-like operators. For comparison operators, either converting signed to unsigned or unsigned to (modular) signed is really bad. (The solution would be (in K&R) to convert both to (signed) `long` but that that would need to be the programmers decision.) – rici Apr 12 '17 at 19:49
  • 1
    Anyway, as I said earlier, the decision that signed->unsigned is well-defined and unsigned->signed is implementation-dependent had already been made. You can argue that a different decision could have been made, but there were a lot of factors militating for this particular decision. You could also argue that C should never have tried to accommodate non-2s-complement machines; in retrospect, that might be reasonable but at the time the future did not seem so one-dimensional. *From the beginning* unsigned was intended to be purely binary 2s complement. – rici Apr 12 '17 at 19:51
  • 1
    It's worth noting that the probably most prominent non-2s complement machine at the time -- the IBM 7090/7094 -- used sign-magnitude representation for arithmetic but also had unsigned carry-propagating addition (and used an unsigned modular adder for address computation as well). So it could have accommodated a C implementation with a single unsigned width and sign-magnitude signed types. But afaik, no C implementation was every written for it. – rici Apr 12 '17 at 19:54
  • 1
    To understand these quotes, you need to appreciate how committees work. The C standard is voted on by a committee, where members have various vested interests that aren't always compatible. Essentially what these quotes are saying is that some voting members voted for "value preserving" conversions and others voted for "sign preserving", and neither side was prepared to budge. Eventually, the camp voting for value preserving rules managed to get the upper hand in voting. So the behaviour we get is due to political rather than technical concerns. – Peter Apr 12 '17 at 20:36
  • @Peter: In the end, one or the other sides was going to prevail, since it is hard to see a "middle ground" between the two proposals. Given your argument, both possibilities represent a *political* victory but presumably one possibility was technically superior (if not, the whole argument is irrelevant now). It may well be that the one eventually chosen is the technically superior one; the fact that the decision mechanism was (even partially) political does not contradict that possibility. – rici Apr 13 '17 at 03:26
  • @Rici - Sure. But, equally, a political decision mechanism does not contradict the opposite. Sometimes political decisions converge on a "technically superior" solution, sometimes they don't. – Peter Apr 13 '17 at 09:26
  • 2
    The reason why unsigned-preserving is generally better is that it is easier/cleaner to write correct mixed operations. For example, comparing and signed and unsigned value correctly for all cases becomes `s < 0 || s < u` or `s >= 0 && s > u`. Both of those rely on the fact that the second comparison converts signed to unsigned if the unsigned to signed conversion would overflow. – Chris Dodd Apr 13 '17 at 17:13
  • 1
    @rici: A middle-ground between the proposals would have been to specify that such conversions may be processed as signed or unsigned, at an implementer's judgment, and used predefined macros to indicate how a particular implementation would treat them. The implementations that would have been most likely to promote short unsigned types to unsigned if allowed to do so, would have been those that don't use quiet wraparound two's-complement semantics. Thus, the rationale for such treatment is a bit fallacious. It's also ironic that "modern" implementations... – supercat Sep 24 '18 at 18:51
  • 1
    ...no longer treat `(unsigned)((unsigned)ushort1)*ushort2` as equivalent to `(unsigned)((int)ushort1*ushort2)`, even though the fact that the majority of current implementations would treat them as equivalent was stated in the rationale as a motivating factor for having short unsigned types promote to signed int. – supercat Sep 24 '18 at 18:53
1

Why does C++ standard specify signed integer be cast to unsigned in binary operations with mixed signedness?

I suppose that you mean converted rather than "cast". A cast is an explicit conversion.

As I'm not the author nor have I encountered documentation about this decision, I cannot promise that my explanation is the truth. However, there is a fairly reasonable potential explanation: Because that's how C works, and C++ was based on C. Unless there was an opportunity improve upon the rules, there would be no reason to change what works and what programmers have been used to. I don't know if the committee even deliberated changing this.


I know what you may be thinking: "Why does C standard specify signed integer...". Well, I'm also not the author of C standard, but there is at least a fairly extensive document titled "Rationale for American National Standard for Information Systems - Programming Language - C". As extensive it is, it doesn't cover this question unfortunately (it does cover a very similar question of how to promote integer types narrower than int in which regard the standard differs from some of the C implementations that pre-date the standard).

I don't have access to a pre-standard K&R documents, but I did find a passage from book "Expert C Programming: Deep C Secrets" which quotes rules from the pre-standard K&R C (in context of comparing the rule with the standardised ones):

Section 6.6 Arithmetic Conversions

A great many operators cause conversions and yield result types in a similar way. This pattern will be called the "usual arithmetic conversions."

First, any operands of type char or short are converted to int, and any of type float are converted to double. Then if either operand is double, the other is converted to double and that is the type of the result. Otherwise, if either operand is long, the other is converted to long and that is the type of the result. Otherwise, if either operand is unsigned, the other is converted to unsigned and that is the type of the result. Otherwise, both operands must be int, and that is the type of the result.

So, it appears that this has been the rule from since before standardisation of C and was presumably the chosen by the designer himself. Unless someone can find a written rationale, we may never know the answer.


What are cases where casting to unsigned in the correct thing to do?

Here is an extremely simple case:

unsigned u = INT_MAX;
u + 42;

The type of the literal 42 is signed, so with your proposed / designer rule, u + 42 would also be signed. This would be quite surprising and would result in the shown program to have undefined behaviour due to signed integer overflow.

Basically, implicit conversion to signed and to unsigned each have their problems.

eerorika
  • 232,697
  • 12
  • 197
  • 326