Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does System.Decimal ignore checked/unchecked context

Tags:

c#

.net

I just stumbled into a System.Decimal oddity once more and seek an explaination.

When casting a value of type System.Decimal to some other type (i. e. System.Int32) the checked keyword and the -checked compiler option seem to be ignored.

I've created the following test to demonstrate the situation:

public class UnitTest
{
    [Fact]
    public void TestChecked()
    {
        int max = int.MaxValue;

        // Expected if compiled without the -checked compiler option or with -checked-
        Assert.Equal(int.MinValue, (int)(1L + max));

        // Unexpected
        // this would fail
        //Assert.Equal(int.MinValue, (int)(1M + max));
        // this succeeds
        Assert.Throws<OverflowException>(() => { int i = (int)(1M + max); });


        // Expected independent of the -checked compiler option as we explicitly set the context
        Assert.Equal(int.MinValue, unchecked((int)(1L + max)));

        // Unexpected
        // this would fail
        //Assert.Equal(int.MinValue, unchecked((int)(1M + max)));
        // this succeeds
        Assert.Throws<OverflowException>(() => { int i = unchecked((int)(1M + max)); });


        // Expected independent of the -checked compiler option as we explicitly set the context
        Assert.Throws<OverflowException>(() => { int i = checked((int)(1L + max)); });

        // Expected independent of the -checked compiler option as we explicitly set the context
        Assert.Throws<OverflowException>(() => { int i = checked((int)(1M + max)); });
    }
}

All my research unitl now didn't lead to a proper explaination for this phenomenon or even some misinformation claiming that it should work. My research already included the C# specification

Is there anybody out there who can shed some light on this?

like image 337
Brar Avatar asked Jan 22 '18 11:01

Brar


Video Answer


3 Answers

The checked context relates to IL emitted from your code - it basically changes the opcode used for those math operations from the unchecked version to the checked version. It can't do that for decimal because decimal isn't a primitive, and has no direct opcodes: all the arithmetic operations are pre-built in custom operators, exactly like they would be if you added your own struct MyType and added operators for it. So: it would all depend on whether the custom operators defined by decimal choose to detect and throw OverflowException or not, in that code. Which you don't control, and can't influence in your build.

It is the decimal type that provides the decimal <===> int conversions. By the time it gets back to your code - where the checked keyword could have an effect - it is already either an int or an exception has been thrown.

The C# custom operator support does not extend to allowing you to add separate checked / unchecked operator implementations, sadly.

like image 97
Marc Gravell Avatar answered Oct 24 '22 20:10

Marc Gravell


C# specification (section 12.7.14 The checked and unchecked operators) contains list of affected operators and statements. Operators in your test aren't in the list:

The following operations are affected by the overflow checking context established by the checked and unchecked operators and statements:

  • The predefined ++ and -- operators (§12.7.10 and §12.8.6), when the operand is of an integral or enumtype.
  • The predefined - unary operator (§12.8.3), when the operand is of an integral type.
  • The predefined +, -, *, and / binary operators (§12.9), when both operands are of integral or enumtypes.
  • Explicit numeric conversions (§11.3.2) from one integral or enumtype to another integral or enumtype, or from float or double to an integral or enumtype.
like image 2
Leonid Vasilev Avatar answered Oct 24 '22 18:10

Leonid Vasilev


The CLR offers IL instructions for simple arithmetic operations like add (addition), sub(subtraction), mul(multiplication), div(division).

For example lets take an add instruction, which adds two values together. The add instruction performs no overflow checking, but there is instruction called add.ovf, which also adds two values together, but will throw an OverflowException if an overflow occurs.

So when you are using checked operator, statement or compiler switch, it will use "overflow checking" version of add instruction (add.ovf).

Remember this only works for a "Primitive Types".

But with decimals things are little different. decimal type is not considered as a primitive type by CLR (although programming languages like c# or visual basic does), which means that CLR does not have IL instructions that know how to manipulate decimal value. If you look for a decimal type in .NET Framework SDK Documantation or on Source Code in ReferenceSources - you will notice, that there is methods called Add, Subtract, Multiply, Divide, etc.. and operator overload methods for +, -, *, /, etc.

When you compile code that uses decimal, the compiler will generate code to call decimal members to perform the actual operation. Also, because there are no IL instructions for manipulating decimal values, the checked/unchecked operators/statements/compiler switches have no effect. Operations with decimal values will always throw an OverflowException if the operation can't be performed safely.

like image 2
SᴇM Avatar answered Oct 24 '22 18:10

SᴇM