Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Arithmetic Overflow Checking using checked/unchecked

0.00/5 (No votes)
20 Jul 2004 1  
How to use overflow checking effectively and avoid some pitfalls.

Introduction

In this article, I thoroughly explore the extremely useful checked and unchecked keywords which control overflow checking. It is slanted towards someone familiar with C and/or C++.

Being able to control overflow checking is one of the many ways that C# improves on C/C++ (and other languages). In theory, C/C++ compilers can do overflow checking for signed integer arithmetic, but the behavior is "implementation defined" according to the C standard. All compilers that I am aware of take the easy way out and ignore overflow conditions - as long as they document the behavior they are considered to be standard conforming. (However, if overflow can occur, you should always use unsigned integer types in C/C++, which are explicitly defined to have unchecked behavior on overflow.)

Most of the time, overflow does not occur, but you sometimes want to check for overflows in case your code has bugs. However, where C# is really useful is when you are expecting a possible overflow. I will discuss all situations in the next sections.

Overflow Not Expected

Often overflow is unimportant since it will not occur. In most code, integer variables take on a very small range of small values. I guess this is why most C/C++ compilers ignore the possibility of overflow.

Of course, even if you don't expect an overflow condition, it may still occur as a result of a bug. For example, it is easy in C to create an infinite loop by attempting to decrement an unsigned integer below zero. In C#, this would generate an overflow exception if overflow checking was on.

In that case, why not always turn overflow checking on when using C#? Well, there is a performance penalty associated with overflow checking. You may not want to slow down your code with overflow checking if you are sure that overflow can never occur.

My recommendation is to never use checked or unchecked if you do not expect an overflow condition. In the release build, turn off overflow checking. Turn it on for debug builds to facilitate spotting and isolating bugs during your testing.

Note that Visual Studio does not turn on overflow checking by default in either release or debug builds. You need to turn on the Check for Arithmetic Overflow setting in the Build Properties for the project - but make sure you have the Debug configuration selected when you do this.

Overflow Expected

There are two situations where overflow is expected:

  1. Overflow needs to be ignored in some algorithms.
  2. Overflow is an error condition (but not a bug) that needs to be explicitly handled.

We will look at the first situation now, and the second situation in the next section.

Many algorithms rely on overflow being ignored. Examples include checksums, and many random number and encryption algorithms. In other words, they rely on arithmetic being performed modulus 2^32 (for 32 bit integers), where any high bits generated are simply discarded.

I once had to translate a PRNG (pseudo random number generator) from C to Pascal. Unfortunately, Pascal always does overflow checking. (I could find no way to turn it off with the compiler I was using.) What was about three lines of code in C expanded to about 2 pages of Pascal code. The extra code was simply to avoid causing an overflow. (Even then, I was not certain the Pascal code would handle all conditions, but I knew the 3 lines of C code would work just by glancing at them.)

Many C/C++ programmers will not realize, at first, how useful the unchecked keyword is because they are used to expressions never being checked, often without even realizing it. For example, I have seen many implementations of GetHashCode that can cause an overflow. Here is an example that correctly turns off overflow checking in the calculation of a hash code.

    public override int GetHashCode()
    {
        return unchecked(name.GetHashCode() + address.GetHashCode());
    }

OverFlow Caught

The second situation, mentioned above, is where you want to detect (not ignore) overflow. It often occurs where the integer values being used are entered or somehow depend on user input. For example, I encountered this situation when I added a calculator to my hex editor (see http://www.hexedit.com).

The calculator needed to tell the user when it has performed an operation that caused an overflow. Unfortunately, the code was in C++ and the compiler provided no support for overflow checking. To detect overflows, the code had to examine the operand(s) for each of the operations and try to determine if an overflow was going to happen. The code to do this is not trivial and increases the chances of new bugs being introduced. In fact, the only known bug that has ever caused HexEdit to crash was due to a divide by zero (only when the user attempted to multiply by zero) in code that was trying to detect when a multiplication in the calculator would cause an overflow!

This is the sort of situation where the C# checked keyword is brilliant. It would be a simple matter of placing all the calculations in a checked block to turn on overflow checking, then wrap the whole thing in a try block that catches OverflowException.

Traps For the Unwary

By now, you may realize that I like the control C# gives over overflow checking. However, there are a few vagaries that you should be aware of.

First, it only works for integer arithmetic. Many may expect it to work for floats, decimals etc., but it doesn't. In fact, overflow in floating point numbers (float and double) will never throw an exception but simply return a special value of +/- Infinity. On the other hand, for the decimal type, the checked/unchecked keywords are also ignored but overflow always throws an OverflowException.

Another thing to watch is that, even for integers, the checked/unchecked keywords have no effect when division by zero is attempted - a DivideByZeroException is always thrown. This may be surprising to some, as they might consider division by zero to be simply a special case of overflow. However, in this case, there is no logical value for the result - even in C, dividing an integer (signed or unsigned) by zero is an error that typically terminates the program.

As an aside, 0/0 (zero divided by zero) for integers does not cause an error (at least on my machine), but gives a result of zero. According to the C# documentation, it should throw a DivideByZeroException. Even more interesting is that the decimal type does throw DivideByZeroException for 0/0. (0/0 for float and double generate a special value called a NaN.)

Conversions between numeric types, where the value to be converted overflows the destination type, can also be checked/unchecked but only if the source is of floating point or integer type and the result is an integer type. If either of the source or destination is a decimal type, the checked/unchecked keywords are ignored and overflow checking is always done. All other conversions will not cause an overflow (although there may be loss of precision), except converting a double to a float which generates a NaN (whereas +/- Infinity might have been expected for consistency).

Another discrepancy is that overflow checking works for simple arithmetic operations (addition, subtraction, and multiplication), but no overflow checking is performed on left shift operations.

For example:

    uint a = uint.MaxValue;
    uint b = checked(a << 1);
    uint c = checked(a * 2);

Since a shift is effectively multiplication by a power of 2, you might expect the above (<< and *) operations to behave identically. However, only the multiplication will cause an exception, while the shift just gives the same result as if the expression were unchecked. I find, this is yet another inconsistency.

Warning

An even bigger trap is that the checked and unchecked statements only apply to the code within the enclosed statement block or expression, and not to any nested function calls. It is important to realize that these keywords only change the code generated by the C# compiler; they do not set or clear any overflow "flag" at run-time. The temptation is to assume that they work similarly to other statements - for example, a try block will catch exceptions generated within any nested function calls. Programmers with a C/C++ background find the syntax misleading - in C/C++, a #pragma would typically be used for this sort of situation.

Note that unchecked and checked blocks are rather like C# unsafe blocks, in that they control code generation, not run-time behavior. The difference with unsafe blocks is that the compiler will generate an error if you try to call a "safe" function from within an unsafe block. But the C# compiler does not even warn if you try to call a function with unchecked expressions from within a checked block, or vice versa.

For example:

    int square(int i)
    {
        return i * i;
    }
    void f()
    {
        checked
        {
            int i = square(1000000);
        }
    }

The code in square() will not be checked when called from f() (unless the global compiler checking flag is on). There is no run-time "flag" to control overflow checking, so a function (like square() above) cannot change its run-time behavior depending on where it is called from. Hence, the above code will not throw an overflow exception as might be expected.

Note that you can inspect the compiled code to see how it changes by looking at the IL code using ILDASM. There are two variations of the relevant commands, without and with overflow checking. For example, the multiply commands are called mul and mul.ovf. (Actually, there are two multiply commands with overflow checking: mul.ovf and mul.ovf.un for signed and unsigned integers.)

Summary

In summary, the C# control of overflow handling is extremely useful where there is possibility of overflow.

My rules of thumb for its use are:

  1. Don't use the unchecked/checked keywords unless an overflow condition is possible.
  2. Use unchecked when you expect overflow but want to ignore it.
  3. Use checked where it is a possible error condition which you want to catch.
  4. Turn on overflow checking globally in debug builds to detect bugs.
  5. Turn off overflow checking globally in release builds for efficiency.

Remember that checked/unchecked only work on the enclosed statements and do not affect nested function calls. Also they only work for simple integer arithmetic (and not shifts) and only for conversions from a real/integer type to a smaller integer type. For the decimal type, overflow checking is always done irrespective of any unchecked keyword, whereas pure floating-point operations are never checked.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here