8

Based on this interesting question: Addition of int and uint and toying around with constant folding as mentioned in Nicholas Carey's answer, I've stumbled upon a seemingly inconsistent behavior of the compiler:

Consider the following code snippet:

int i = 1;
uint j = 2;
var k = i - j;

Here the compiler correctly resolves k to long. This particular behavior is well defined in the specifications as explained in the answers to the previously referred question.

What was surprising to me, is that the behavior changes when dealing with literal constants or constants in general. Reading Nicholas Carey's answer I realized that the behavior could be inconsistent so I checked and sure enough:

const int i = 1;
const uint j = 2;
var k = i - j; //Compile time error: The operation overflows at compile time in checked mode.
k = 1 - 2u; //Compile time error: The operation overflows at compile time in checked mode.

k in this case is resolved to Uint32.

Is there a reason for the behavior being different when dealing with constants or is this a small but unfortunate "bug" (lack of a better term) in the compiler?

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
InBetween
  • 32,319
  • 3
  • 50
  • 90
  • 2
    At a guess, the compiler doesn't do implicit conversions for constants. – Powerlord Oct 15 '14 at 15:31
  • @Powerlord Well it has to, after all it *is* implicitly converting `int` to `uint`. – InBetween Oct 15 '14 at 15:33
  • 1
    The spec allows for that, though... §6.1.9 Implicit constant expression conversions: "A constant-expression (§7.19) of type int can be converted to type `sbyte`, `byte`, `short`, `ushort`, `uint`, or `ulong`, provided the value of the constant-expression is within the range of the destination type." Still trying to find what it says for literals and/or constant variables. – Powerlord Oct 15 '14 at 15:37

2 Answers2

4

From the C# specification version 5, section 6.1.9, Constant Expressions only allow the following implicit conversions

6.1.9 Implicit constant expression conversions
An implicit constant expression conversion permits the following conversions:
* A constant-expression (§7.19) of type int can be converted to type sbyte, byte, short, ushort, uint, or ulong, provided the value of the constant-expression is within the range of the destination type.
• A constant-expression of type long can be converted to type ulong, provided the value of the constant-expression is not negative.

Note that long is not on the list of int conversions.

The other half the problem is that only a small number of numeric promotions happen for binary operations:

(From Section 7.3.6.2 Binary numeric promotions):

  • If either operand is of type decimal, the other operand is converted to type decimal, or a binding-time error occurs if the other operand is of type float or double.
  • Otherwise, if either operand is of type double, the other operand is converted to type double.
  • Otherwise, if either operand is of type float, the other operand is converted to type float.
  • Otherwise, if either operand is of type ulong, the other operand is converted to type ulong, or a binding-time error occurs if the other operand is of type sbyte, short, int, or long.
  • Otherwise, if either operand is of type long, the other operand is converted to type long.
  • Otherwise, if either operand is of type uint and the other operand is of type sbyte, short, or int, both operands are converted to type long.
  • Otherwise, if either operand is of type uint, the other operand is converted to type uint.
  • Otherwise, both operands are converted to type int.

REMEMBER: The int to long conversion is forbidden for constants, meaning that both args are instead promoted to uints.

Powerlord
  • 87,612
  • 17
  • 125
  • 175
  • Incidentally, there's no indication of why `int` constant expressions can't be converted to `long`s... or any discussion of whether `uint` constants can be converted at all... and no explanation of why in either of these cases. – Powerlord Oct 15 '14 at 16:08
3

Check out this answer here

The problem is that you are using const.

At run time when there is a const the behavior is exactly as with literals, or as if you had simply hard coded those numbers in the code, so since the numbers are 1 and 2 it casts to a Uint32 since 1 is within the range of uint32. Then when you try to subtract 1 - 2 with uint32 it overflows, since 1u - 2u = +4,294,967,295 (0xFFFFFFFF).

The compiler is allowed to look at litterals, and interpret them different than it would other variables. Since const will never change, it can make guarantees that it otherwise couldn't make. in this instance it can guarentee that 1 is within the range of a uint, therfore it can cast it implicitly. In normal circumstances(without the const) it cannot make that guarantee,

a signed int ranges from -2,147,483,648 (0x80000000) to +2,147,483,647 (0x7FFFFFFF).

an unsigned int ranges from 0 (0x00000000) to +4,294,967,295 (0xFFFFFFFF).

Moral of the story, be careful when mixing const and var, you may get something you don't expect.

Community
  • 1
  • 1
Jared Wadsworth
  • 839
  • 6
  • 15
  • Right, derp, the problem is with the `var`, not the constant. Chalk one up for going down the wrong path when looking through the spec. – Powerlord Oct 15 '14 at 15:41
  • using `var` here is not the issue as it's a compile time error. In real code I'd never use `var` in an expression where the type is not crystal clear at first glance. And I know that using `const` or *constant literales* will have the same behavior. My question is *why* that behavior is inconsistent with the general one where variables are involved. – InBetween Oct 15 '14 at 15:42
  • @Powerlord No, `var` is not the problem. `long k = 1 - 2u;` is also a compile time error. In C# the return type never plays a role in overload resolution so why would `var` be part of the issue? The problem is why `1 - 2u` is resolving to `uint` when dealing with *constants*. – InBetween Oct 15 '14 at 15:44
  • because when you use constants the compiler treats them as literals, meaning the compiler looks at them and says, thats a uint. meaning that when you tried to subtract a larger uint from a smaller one, it overflows. with uints 1 - 2 = +4,294,967,295 (0xFFFFFFFF) – Jared Wadsworth Oct 15 '14 at 15:45
  • @JaredWadsworth: OK, id understand that if I were subtracting *two* `uint`s. But I'm subtracting an `int` and a `uint` so the compiler interpreting that I want `uint` as a return type is *wrong*. It doesn't do that when not dealing with constants. Why change behavior? – InBetween Oct 15 '14 at 15:47
  • 1
    @InBetween sorry, but compilers are never wrong. It interprets 1 as a uint, and 2 as a uint when using constants, because it places them in as LITTERALS. This only happens when using constants to ensure that the constant never gets changed (ie. remains constant). 1 is within the range of a uint, so it gets converted to a uint, and then the subtraction takes place. The reason it becomes a long if they are not const is because the compiler cannot guarantee that the int is within the range of the uint. with the const modifier it can. – Jared Wadsworth Oct 15 '14 at 15:51
  • I updated my answer to better reflect this principle – Jared Wadsworth Oct 15 '14 at 16:06
  • ok, I understand that, but if the compiler can make all the garantees it wants with constants at compile time then why can't it figure out that the addition won't fit in a `uint` to begin with and solve the problem the same way it would at runtime? I guess it's just not worth it. – InBetween Oct 15 '14 at 16:55
  • The problem comes because of the order of operations. Casting always comes before subtraction. The compiler sees that it can be a uint to match both sides, and casts it. Then it looks to see what operation it needs to perform, in this case subtraction. – Jared Wadsworth Oct 15 '14 at 16:59