Tuesday, January 17, 2023

Checking for Overflow in C#

By default, integral values (such as int, uint, and long) do not throw an exception when they overflow. Instead, they "wrap" -- and this is probably not a value that we want. But we can change this behavior so that an exception is thrown instead.

Short version:

Use a "checked" statement to throw an overflow exception when an integral overflow (or underflow) occurs.

As an alternate, there is also a project property that we can set: CheckForOverflowUnderflow.

Note: This behavior applies to "enum" and "char" which are also integral types in C#.

Let's take a closer look at this.

Default Behavior

Let's start by looking at the default overflow behavior for integer (Int32) -- this applies to other integral types as well.

    int number = int.MaxValue;
    Console.WriteLine($"number = {number}");

    number++;
    Console.WriteLine($"number = {number}");

This creates a new integer variable ("number") and sets its value to the maximum value that a 32-bit integer can hold.

When we increment that value (using the ++ operator), it goes past that maximum value. But instead of throwing an error, the value "wraps" to the lowest integer value.

Here is the output of the above code:

    number = 2147483647
    number = -2147483648

This is probably not the behavior that we want.

Using a "checked" Statement

One way we can fix this is to use a "checked" statement. This consists of the "checked" operator and a code block.

    int number = int.MaxValue;
    Console.WriteLine($"number = {number}");
    checked
    {
        number++;
    }
    Console.WriteLine($"number = {number}");
  

The result of this is that the increment operation is checked to see if it will overflow. If it does, it throws an OverflowException.

Here's the exception if we run this code from Visual Studio:

    System.OverflowException: 'Arithmetic operation resulted in an overflow.'

Let's handle the exception:

    try
    {
        int number = int.MaxValue;
        Console.WriteLine($"number = {number}");

        checked
        {
            number++;
        }
        Console.WriteLine($"number = {number}");
    }
    catch (OverflowException ex)
    {
        Console.WriteLine($"OVERFLOW: {ex.Message}");
    }

Now our output looks like this:

    number = 2147483647
    OVERFLOW: Arithmetic operation resulted in an overflow.

If we overflow an integer value, it probably means that we need to make some changes to the code (such as using a larger type). But the good news is that an overflow exception makes sure that we do not unintentionally use an invalid value.

Using "CheckForOverflowUnderflow"

We may want to apply overflow checks through our entire project. Instead of using individual "checked" statements, we can also set a property on the project: "CheckForOverflowUnderflow".

To see this in action, we will removed the "checked" statement from our code:

    try
    {
        int number = int.MaxValue;
        Console.WriteLine($"number = {number}");

        number++;
        Console.WriteLine($"number = {number}");
    }
    catch (OverflowException ex)
    {
        Console.WriteLine($"OVERFLOW: {ex.Message}");
    }

The code does not throw an exception, and we are back to the wrapped value:

    number = 2147483647
    number = -2147483648

In the project, we can add the "CheckForOverflowUnderflow" property. Here is the project file for our basic console application:

    <Project Sdk="Microsoft.NET.Sdk">

      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <RootNamespace>check_overflow</RootNamespace>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
      </PropertyGroup>

    </Project>

We've set the "CheckForOverflowUnderflow" property to "true". Now when the application runs, the exception is thrown.

    number = 2147483647
    OVERFLOW: Arithmetic operation resulted in an overflow.

"unchecked"

In addition to "checked", there is also an "unchecked" operator. As you might imagine, this does not check for overflow.

So with our project set to "CheckForOverflowUnderflow", we can add an "unchecked" block that will ignore the project setting.

    try
    {
        int number = int.MaxValue;
        Console.WriteLine($"number = {number}");

        unchecked
        {
            number++;
        }
        Console.WriteLine($"number = {number}");
    }
    catch (OverflowException ex)
    {
        Console.WriteLine($"OVERFLOW: {ex.Message}");
    }

The code does not throw an exception, and we are back to the wrapped value:

    number = 2147483647
    number = -2147483648

Wrap Up

Normally, I do not need to worry about overflow or underflow in my code; it's not something that comes up in my applications very often.

One exception is with Fibonacci sequences. I've written a few articles involving Fibonacci sequences (including "Implementing a Fibonacci Sequence with Value Tuples in C# 7" and "Coding Practice: Learning Rust with Fibonacci Numbers"). Since the sequence more or less doubles on each item, it overflows a 32-bit integer very quickly (around the 46th item in the sequence). This is one place where I generally use a larger type (like a long) and also a "checked" statement to make sure I do not end up using invalid values in my code somewhere.

"checked" statements do come with a cost. There is a little overhead that is added in the arithmetic operations. Because of this, I generally leave projects with the default setting ("unchecked" for the project), and then use targeted "checked" statements where I need them.

It's always interesting to find things like this in the C# language. I don't often need it, but it's really good to have it when I do.

Happy Coding!

No comments:

Post a Comment