The scope of the C# checked/unchecked keyword is static, not dynamic


C# has operators checked and unchecked to control the behavior of the language in the face of integer overflow. There are also checked and unchecked statements which apply the behavior to blocks of statements rather than single expressions.

int x;

x = checked(a + b); // evaluate with overflow checking
y = unchecked(a + b); // evaluate without overflow checking

checked {
 x = a + b; // evaluate with overflow checking
}

unchecked {
 x = a + b; // evaluate without overflow checking
}

Why, then, doesn't this code below raise an overflow exception?

class Program {
 static int Multiply(int a, int b) { return a * b; }
 static int Overflow() { return Multiply(int.MaxValue, 2); }

 public static void Main() {
  System.Console.WriteLine(checked(Overflow()));
  checked {
    System.Console.WriteLine(Overflow());
  }
 }
}

(Mini-exercise: Why couldn't I have just written static int Overflow() { return int.MaxValue * 2; }?)

The answer is that the scope of the checked or unchecked keyword is static, not dynamic. Whether a particular arithmetic is checked or unchecked is determined at compile time, not at run time. Since the multiplication in the Multiply function is not explicitly marked checked or unchecked, uses the overflow context implied by your compiler options. Assuming you've left it at the default of unchecked, this means that there is no overflow checking in the Multiply function, even if you call it from a checked context. Because once you call the Multiply function, you have left the checked context.

The C# language specification addresses this issue not once, not twice, but three times! (But it seems that some people miss it, possibly because there is too much documentation.)

First, there is an explicit list of operations which are controlled by the checked or unchecked keyword:

  • The predefined ++ and -- unary operators, when the operand is of an integral type.

  • The predefined - unary operator, when the operand is of an integral type.

  • The predefined +, -, *, and / binary operators, when both operands are of integral types.

  • Explicit numeric conversions from one integral type to another integral type, or from float or double to an integral type.

That's all. Note that function calls are not on the list.

Now, that may have been a bit too subtle (documentation by omission), so the language specific goes ahead and calls it out.

The checked and unchecked operators only affect the overflow checking context for those operations that are textually contained within the "(" and ")" tokens. The operators have no effect on function members that are invoked as a result of evaluating the contained expression.

And then, in case you still didn't get it, the language specification even includes an example:

class Test
{
   static int Multiply(int x, int y) {
      return x * y;
   }
   static int F() {
      return checked(Multiply(1000000, 1000000));
   }
}

The use of checked in F does not affect the evaluation of x * y in Multiply, so x * y is evaluated in the default overflow checking context.

(I wrote my example before consulting the language specification. That we both chose to use multiplication overflow is just a coincidence.)

Even though the language specification says it three times, in three different ways, there are still people who are under the mistaken impression that the scope of the checked keyword is dynamic.

Another thing you may have notice is that the checked and unchecked keywords apply only to the built-in arithmetic operations on integers. They do not apply to overloaded operators or to operators on custom classes.

Which makes sense if you think about it, because in order to define an overloaded operator or an operator on a custom class, you need to write the implementation as a separate function, in which case you have already left the scope of the checked and unchecked keywords.

And now we are leaving the scope of CLR Week. You can remove your hands from your ears now.

Comments (21)
  1. Damien says:

    Mini-Exercise – that would be a constant expression evaluated at compile time, not runtime.

  2. OldFart says:

    I wish one week a month could be CLR Week.

  3. Nick says:

    I would like to leave the unmanaged scope.

  4. Adam Rosenfield says:

    If you think about this from the JIT compiler's point of view, it's pretty obvious.  If an arithmetic expression is unchecked, compile it directly to an add/mul/whatever opcode.  If the expression is checked, compile it to an add/mul and put in a "jo ThrowOverthrowException" instruction afterwards (jump-if-overflow-flag-is-set, or whatever your ISA's equivalent of that is for non-x86).

    x86 doesn't have any kind of global processor state that says "raise an interrupt if an arithmetic overflow occurs while this flag is set," which could be used if checked/unchecked were dynamic in scope.  And implementing that functionality in software would be prohibitively expensive—every arithmetic operation would have to check that checked/unchecked state.  So now your simple add instruction is now an add, a load (probably from TLS or a dedicated register which can no longer be used by the application), a cmp, and a jCC.

  5. 12BitSlab says:

    @ OldFart,

    I will second that motion.

    Question for Raymond: I understand you aren't the biggest fan of the managed code environment.  Can you shed some light on your thoughts in that regard?

    [You understand incorrectly. I don't write about it because managed code is not my area of expertise, and because focusing on Win32 gives me a more exclusive niche; not because I don't like managed code. -Raymond]
  6. Myria says:

    Does C# define int.MinValue / -1 as throwing an exception on all architectures?  I don't know C# well enough to know the rules at this level.  (I did write a PE loader in C# using P/Invoke without unsafe types, though, which was pretty crazy.)

    Does C# promote everything smaller than "int" to "int"?  And if so, does it behave like C/C++, where unsigned integers smaller than "int" promote first to "signed int" rather than "unsigned int"?

  7. 12BitSlab says:

    @ Raymond, thanks for the clarification!

  8. JamesNT says:

    While I certainly do enjoy Mr. Chen's articles on unmanaged code, I do assert that I also thoroughly enjoy his articles on Windows' history and I do enjoy CLR week.  It would not bother me at all if Mr. Chen "evened things out" a bit more (e.g. there were suddenly three CLR weeks a year and more articles on history).

    JamesNT

  9. Myria says:

    @JamesNT: I almost never work with .NET, but don't mind Raymond's occasional forays into Manageland.  I always learn interesting things in them! =^-^=

  10. Azarien says:

    How about a CLR Month?

  11. Smeargle235 says:

    @Myria

    C# does not define any operators for any type smaller than int (32-bit signed integer). Therefore, any 16- or 8-bit integer, signed or unsigned, will be automatically promoted to int before any arithmetic is done.

    uint (unsigned 32-bit int), long (signed 64-bit int), and ulong (unsigned 64-bit int) have their own operators defined, and will return the appropriate type. I can't tell you off the top of my head what type an operation between a uint and a signed int will return, but the spec makes sure to say that an operation between a signed and unsigned long will cause an error.

    Source: ECMA-334 §14.7.4

  12. ben says:

    >Which makes sense if you think about it, because in order to define an overloaded operator or an operator on a custom class, you need to write the implementation as a separate function, in which case you have already left the scope of the checked and unchecked keywords.

    That does not make sense if you continue thinking.

    Theoretically it could allow you to write two different methods for every overloaded operator, one used in the unchecked mode and the other in the checked mode

  13. voo says:

    @Smeargle235 c# actually has sane rules for dealing with types of different signedness.

    uint + int = long   while

    ulong + long = compiler error

    Knuth be thanked for that, c's rule for integer promotion are horribly arcane – the right thing is to force the user to be explicit if you can't guarantor that the result is intuitively correct.

  14. user says:

    Off topic

    Raymond you always write about bad practices some people take when coding and most of them are funny, I want to thank you for that, but I also like to ask you to tell us about great works you witnessed people outside Microsoft have done.

    Thanks.

    [Few people contact Microsoft to say, "Here's something awesome we did. Please review it in order to confirm its awesomeness." -Raymond]
  15. KJK::Hyperion says:

    I'd be really nervous if someone could change a global setting like "checked integer operations" before calling my code that was written assuming a different value of the setting

  16. The_Assimilator says:

    Raymond, for someone who claims "managed code is not my area of expertise", you sure do a good job of making the exact opposite appear true. Thanks for CLR Week 2014, and as other posters have noted, a more frequent version would be something greatly appreciated.

    [You're seeing selection bias. I have an entire year to find five things to look smart about. If CLR Week were more frequent, the quality would be lower. -Raymond]
  17. Medinoc says:

    @Myria, Smeargle235: It becomes nonsensical when you try to add (or worse, apply a bitwise operator to) (u)short or byte numbers. "ushort a=3, b=5, c; c = a & b" will cause a compilation error.

    But "ushort a=3, b=5, c; c = a; c &= b" will not: the assignment+operation operators automatically include a cast (meaning the operands will be promoted, the operation evaluated, and then truncated).

  18. voo says:

    @medinoc that has nothing to do with signedness – both java and c# widen input to at least int sizes, which I think mostly has to do with implementation simplicity (also saves byte codes do denser code).

    Also tbh I have no idea what c does there, I seriously can't remember when I had smaller than int sized operands for bit ops.

    I agree that the behavior for the compound operands is horrible though!

  19. Joe says:

    I don't really understand how static/dynamic scope applies to the checked statement – But when you read the documentation it does make sense – The overflow has already occured in multiply() and a function call is not listed as something checked works on anyway.

  20. Joshua says:

    [Few people contact Microsoft to say, "Here's something awesome we did. Please review it in order to confirm its awesomeness." -Raymond]

    I could if you wanted. Although in this case it is peverse awesomeness.

  21. Adam Rosenfield says:

    @voo: C has mostly the same behavior—for integer types smaller int, it widens the operands to int (or unsigned int in rare cases) as part of the *usual arithmetic conversions* (it also then converts both operands to a common type, in case they differ in signedness or if either operand is wider than int).  From C99 §6.3.1.8/1:

    "First, [some cases about long double, double, and float].  Otherwise, the integer promotions are performed on both operands. Then the following rules are applied to the promoted operands: […]"

    §6.3.1.1/2, on integer promotions:

    "If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions."

Comments are closed.