< C++ .NET Exception Handling 1 | Main | C++ .NET Exception Handling 3 >


 

 

Exception Handling 2

 

 

  1. Handling Exceptions

  2. Using the try-catch Construct

  3. Customizing Exception Handling

  4. Using the Exception Hierarchy

  5. Using Exceptions with Constructors

 

 

Handling Exceptions

 

Now that you’ve seen how to generate exceptions, let’s move on to handling them.

 

Using the try-catch Construct

 

Exceptions are caught and processed using the try and catch construct, which has the following form:

try

{

    // code that may fail

}

catch(TypeOne^ pOne)

{

    // handle this exception

}

catch(TypeTwo^ pTwo)

{

    // handle this exception

}

Code that you suspect might fail is enclosed in a try block that is followed by one or more handlers in the form of catch blocks. Each catch block looks a little like a function definition, with catch followed by a type in parentheses, which represents the type that will be caught and processed by the catch block. In the preceding code, the first catch block will handle exceptions tagged with a TypeOne^ type, while the second block will handle those tagged with a TypeTwo^ type.

Try and catch blocks form a single construct. You can’t have a try block without at least one catch block, you can’t have a catch block without a try block, and you can’t put anything in between them. You can chain as many catch blocks together as there are exception types to catch, as long as you have at least one. The following exercise will show you the basics of handling exceptions, using the example from the previous exercise as a basis.

 

1.        Reopen the project from the previous example if you’ve closed it. Modify the main() function so that it looks like this:

Console::WriteLine(L"Throw Test");

try

{

   int n = 3;

   Console::WriteLine(L"Calling with n = 3");

   func(n);

   Console::WriteLine(L"Calling with n = 0");

   n = 0;

   func(n);

}

catch(System::ArgumentException^ pex)

{

   Console::WriteLine(L"Exception was {0}", pex);

}

Console::WriteLine(L"All done");

 

Exception handling - Adding the try and catch block in the main project file

 

The calls to the function are enclosed in a try block, which is followed by a single catch block. When the second call to the function fails, the exception-handling mechanism takes over. It can’t find a handler in the function where the error originated, so it walks one level up the call stack and comes out in the try block. At this point, the runtime wants to go off looking for a handler. As part of this process, it puts the program stack back to where it was at the start of the try block. In other words, it unwinds the stack, which means that it destroys any variables that have been created on the stack within the try block, so you can’t use them in the catch block. You need to bear this in mind when writing exception handlers and declare any variables you need to use in the catch block outside the corresponding try. When the stack has been unwound, the code looks at the catch blocks associated with this try block to see whether there is one that has an argument type that matches what was thrown. In this case, we have a match, so the contents of the catch block are executed. If there wasn’t a suitable catch block, the runtime would try to move up another level of the call stack and then would fail and terminate the program.

 

2.        Execute this code. You should see something very similar to the following figure. Take note that the JIT Debugger not invoked anymore.

 

 

 

 

 

 

The exception handling - the exception has been catched successfully

 

The second function call has generated an exception that has been caught by the catch block, which has printed out “Exception was:” plus the exception details. In contrast to what happened in the previous exercise, the final “All done” message is now printed. This illustrates an important point about exception handling: once a catch block has been executed, program execution continues after the catch block as if nothing had happened. If there are any other catch blocks chained to the one that is executed, they’re ignored.

 

3.        Try changing the second call so that it passes in a positive value. You’ll find that the catch block isn’t executed at all as shown below when the second call, n = 4.

 

Exception handling - the program run smoothly when there is no exception

 

If a try block finishes without any exception occurring, execution skips all the catch blocks associated with the try block.

 

Customizing Exception Handling

 

Just printing out the exception object results in the type-plus-message-plus-stack trace that you saw when the exception was unhandled. You can use properties of the Exception class to control what is printed, as shown in the following table.

 

System::Exception Property

Description

Message

Returns a string containing the message associated with this exception.

StackTrace

Returns a string containing the stack trace details.

Source

Returns a string containing the name of the object or application that caused the error. By default, this is the name of the assembly.

 

Table 2

 

If you altered the WriteLine statement in the catch block to read like this:

catch(System::ArgumentException^ pex)

{

    Console::WriteLine(L"Exception was {0}", pex->Message);

}

you’d expect to see a result like this:

Exception was Aaargh! What wrong?

In a similar way, you could use StackTrace to retrieve and print the stack trace information.

 

Using the Exception Hierarchy

 

The exception classes form a hierarchy based on System::Exception, and you can use this hierarchy to simplify your exception handling. As an example, consider System::ArithmeticException, which inherits from System::Exception and has subclasses that include System::DivideByZeroException and System::OverflowException. Now look at the following code:

try

{

    // do some arithmetic operation

}

catch(System::ArithmeticException^ pex)

{

    // handle this exception

}

catch(System::DivideByZeroException^ pex)

{

    // handle this exception

}

Suppose a DivideByZeroException is thrown. You might expect it to be caught by the second catch block, but it will, in fact, get caught by the first one. This is because, according to the inheritance hierarchy, a DivideByZeroException is an ArithmeticException, so the type of the first catch block matches. To get the behavior you expect when using more than one catch block, you need to rank the catch blocks from most specific to most general. The compiler will give you warning C4286 if you get the catch blocks in the wrong order. This works for both managed and unmanaged code. So, if you just want to catch all arithmetic exceptions, you can simply put in a handler for ArithmeticException, and all exceptions from derived classes will get caught. In the most general case, you can simply add a handler for Exception, and all managed exceptions will be caught.

 

Using Exceptions with Constructors

 

One of the advantages of exceptions mentioned before is: they enable you to signal an error where there’s no way to return a value. They’re very useful for reporting errors in constructors, which, as you now know, don’t have a return value. In the following exercise, you’ll see how to define a simple class that uses an exception to report errors from its constructor, and you’ll also see how to check for exceptions when creating objects of this type.

 

1.        Start Visual C++/Studio .NET, and create a new CLR Console Application project named CtorTest.

 

Creating new CLR Console Application C++ .NET project

 

2.        Immediately after the using namespace System; line and immediately before main(), add the following class definition:

ref class Test

{

    String^ pv;

    public:

    Test(String^ pval)

    {

        // test for null pointer or empty string

        if (pval == nullptr || pval == L"")

          throw gcnew System::ArgumentException(L"Argument null or blank");

        else

          pval = pv;

    }

};

The ref keyword makes this class managed, and this managed class has one simple data member, a pointer to a managed String. At construction time, this pointer must not be null or point to a blank string, so the constructor checks the pointer and throws an exception if the test fails. If the pointer passes the test, construction continues.

 

3.        Try creating an object in the main() function, like this:

int main(array<System::String ^> ^args)

{

    Console::WriteLine(L"Exceptions in Constructors");

    // Create a null pointer to test the exception handling

    String^ ps = nullptr;

    Test^ pt = nullptr;

    // Try creating an object

    try

    {

        pt = gcnew Test(ps);

    }

    catch(System::ArgumentException^ pex)

    {

        Console::WriteLine(L"Exception: {0}", pex->Message);

    }

    Console::WriteLine(L"Object construction finished");

    return 0;

}

  Creating an object in the main() function that include the try-catch block

 

Notice that the call to gcnew is enclosed in a try block. If something is wrong with the String pointer (as it is here), the Test constructor will throw an exception that will be caught by the catch block.

 

4.        Build and run your program and the following output should be expected.

 

Exception handling - Console output program example for multiple catch

 

5.        Try modifying the declaration of the ps string so that it points to a blank string (initialize it with L"") as shown below.

 

Exception handling for empty string

 

Exception handling for empty string program output

 

6.        And then try a non-blank string, to check that the exception is thrown correctly as shown below.

 

Exception handling - non-blank string, to check that the exception is thrown correctly

 

Exception handling - with non-blank string console output

 

 

 

 

Part 1 | Part 2 | Part 3 | Part 4 | Part 5 | Part 6 | Part 7

 


 

< C++ .NET Exception Handling 1 | Main | C++ .NET Exception Handling 3 >