Reinventing The Wheel

How "checked" Works Behind The Scenes

If you haven't heard of it before, "checked" is a keyword in C# that "checks" for an overflow and throws an Exception if one occurs.

For example have a look at the following code:

class Program
{
    static void Main(string[] args)
    {
        checked
        {
            Byte b = 105;
            b += 205;
        }
    }
}

If you run the above code in a console app, an OverflowException will be thrown. Try commenting out "checked" and run the code again, you'll see that no Exception is thrown.

Now have a look at the following code:

class Program
{
    static void Main(string[] args)
    {
        checked
        {
            addSomeBytes();
        }
    }
    static void addSomeBytes()
    {
        Byte b = 105;
        b += 205;
    }
}

Running this code should produce the same result as before, right?

Turns out it doesn't. No Exception is thrown, and this is because of how "checked" works "behind the scenes".

Let's have a look at the IL code that gets generated from the first code block if you comment out "checked".

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       14 (0xe)
  .maxstack  2
  .locals init ([0] uint8 b)
  IL_0000:  nop
  IL_0001:  ldc.i4.s   105
  IL_0003:  stloc.0
  IL_0004:  ldloc.0
  IL_0005:  ldc.i4     0xcd
  IL_000a:  add
  IL_000b:  conv.u1
  IL_000c:  stloc.0
  IL_000d:  ret
// end of method Program::Main

Just as you'd expect, the IL terms "add" and "conv" are used. Nothing amazing here.

Now let's look at the same code with "checked" not commented out.

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       16 (0x10)
  .maxstack  2
  .locals init ([0] uint8 b)
  IL_0000:  nop
  IL_0001:  nop
  IL_0002:  ldc.i4.s   105
  IL_0004:  stloc.0
  IL_0005:  ldloc.0
  IL_0006:  ldc.i4     0xcd
  IL_000b:  add.ovf
  IL_000c:  conv.ovf.u1
  IL_000d:  stloc.0
  IL_000e:  nop
  IL_000f:  ret
// end of method Program::Main

As you can see, basically the only difference is the use of "add.ovf" and "conv.ovf". These two IL functions do the same as "add" and "conv", except they throw an Exception when an overflow is detected.

Because all "checked" does is use the "ovf" versions for "add" etc when they are directly in the "checked" block, no overflow is checked when a method is called that throws an Overflow exception!

What about Decimal and BigInteger?

Have a look at this code:

class Program
{
    static void Main(string[] args)
    {
        unchecked
        {
            decimal maxValue = decimal.MaxValue;
            decimal x = 3;
            decimal sum = maxValue + x;
        }           
    }
}

This is using "unchecked", which means it should not check for overflows, right?

Turns out no matter what, if there is an overflow with a Decimal type, it will be detected and an Exception will be thrown.

To understand why this is, let's look at the generated IL code:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       30 (0x1e)
  .maxstack  5
  .locals init ([0] valuetype [mscorlib]System.Decimal maxValue,
           [1] valuetype [mscorlib]System.Decimal x,
           [2] valuetype [mscorlib]System.Decimal sum)
  IL_0000:  nop
  IL_0001:  nop
  IL_0002:  ldc.i4.m1
  IL_0003:  ldc.i4.m1
  IL_0004:  ldc.i4.m1
  IL_0005:  ldc.i4.0
  IL_0006:  ldc.i4.0
  IL_0007:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32,
                                                                     int32,
                                                                     int32,
                                                                     bool,
                                                                     uint8)
  IL_000c:  stloc.0
  IL_000d:  ldc.i4.3
  IL_000e:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32)
  IL_0013:  stloc.1
  IL_0014:  ldloc.0
  IL_0015:  ldloc.1
  IL_0016:  call       valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Addition(valuetype [mscorlib]System.Decimal,
                                                                                                valuetype [mscorlib]System.Decimal)
  IL_001b:  stloc.2
  IL_001c:  nop
  IL_001d:  ret
// end of method Program::Main

As you can see, although the decimal type is considered a primitive type in C#, the CLR doesn't consider it a primitive type and therefore doesn't have IL instructions to manipulate it.

Instead, the decimal type has its own methods for addition, subtraction etc.

Because of this, checked/unchecked has no effect on a decimal type.

Since BigInteger has no theoretical upper/lower limit, checked/unchecked also has no effect on BigInteger instances.