Zach Saw Zach Saw - 3 years ago 110
C# Question

Integral type promotion inconsistency

using System;

public class Tester
{
public static void Main()
{
const uint x=1u;
const int y=-1;
Console.WriteLine((x+y).GetType());
// Let's refactor and inline y... oops!
Console.WriteLine((x-1).GetType());
}
}


Imagine the code above being used in the following case:

public long Foo(uint x)
{
const int y = -1;
var ptr = anIntPtr.ToInt64() + (x + y) * 4096;
return ptr;
}


It looks like it's perfectly safe to inline
y
, but it's actually not. This inconsistency in the language itself is counter-intuitive and is plain dangerous. Most programmers would simply inline
y
, but you'd actually end up with an integer overflow bug. In fact, if you write code such as the above, you'd easily have the next person working on the same piece of code inline
y
without even thinking twice.

I argue that this is a very counter-productive language design issue of C#.

First question, where is this behaviour defined in the C# specs and why was it designed this way?

Second question,
1.GetType()
/
(-1).GetType()
gives
System.Int32
. Why then is it behaving differently to
const int y=-1
?

Third question, if it implicitly gets converted to
uint
, then how do we explicitly tell the compiler it's a signed integer (
1i
isn't a valid syntax!)?

Last question, this can't be a desired behaviour intended by the language design team (Eric Lippert to chime in?), can it?

Answer Source

This behaviour is described by section 6.1.9 of the C# standard, 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.

So you have const uint x = 1u; and the constant expression (x - 1).

According to the specification, the result of that x - 1 would normally be int, but because the value of the constant expression (i.e. 0) is within range of uint it will be treated as uint.

Note that here the compiler is treating the 1 as unsigned.

If you change the expression to (x + -1) it treats the -1 as signed and changes the result to int. (In this case, the - in -1 is a "unary operator" which converts the type of the result of -1 to int, so the compiler can no longer convert it to uint like it could for plain 1).

This part of the specification implies that if we were to change the constant expression to x - 2 then the result would no longer be a uint but would instead be converted to int. However, if you make that change you get a compile error stating that the result would overflow a uint.

That's because of another part of the C# spec, in section 7.19 Constant Expressions which states:

The compile-time evaluation of constant expressions uses the same rules as run-time evaluation of non-constant expressions, except that where run-time evaluation would have thrown an exception, compile-time evaluation causes a compile-time error to occur.

In this case, there would have been an overflow if doing a checked calculation, so the compiler balks.


With regard to this:

const uint x = 1u;
const int y = -1;
Console.WriteLine((x + y).GetType()); // Long

That's the same as this:

Console.WriteLine((1u + -1).GetType()); // Long

This is because the -1 is of type int and the 1u is of type uint.

Section 7.3.6.2 Binary numeric promotions describes this:

• 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.

(I omitted the part not relevant to this specific expression.)


Addendum: I just wanted to point out a subtle difference in the unary minus (aka "negation") operator between constant and non-constant values.

According to the standard:

If the operand of the negation operator is of type uint, it is converted to type long, and the type of the result is long.

That is true for variables:

var p = -1;
Console.WriteLine(p.GetType()); // int

var q = -1u;
Console.WriteLine(q.GetType()); // long

var r = 1u;
Console.WriteLine(r.GetType()); // uint

Although for compile-time constants the value of 1 is converted to uint if an expression involving uint is using it, in order to keep the whole expression as a uint, -1 is actually treated as an int.

I do agree with the OP - this is very subtle stuff, leading to various surprises.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download