Managed C++ and .NET Development: Visual Studio .NET 2003 Edition

Error handling should be nothing new to software developers. All programmers have written code that verifies that the processes in their code work properly and, if they don't, does something special to correct them. Wouldn't it be nice if nothing could go wrong with your programs and you could write code without having to worry about whether something might go wrong?

Well, you can use exceptions to do that—sort of. Along with the exception's normal role of handling all unforeseen problems, it can actually allow you to code in a manner as if nothing will go wrong and then capture all the possible errors at the end. This separation of error handling from the main code logic can make the program much easier to work with. It eliminates multiple layers of if statements with the sole purpose of trapping errors that might occur but most probably won't.

With Managed C++, exceptions have been taken one step further than with traditional C++. Exceptions can now be thrown across language boundaries. That means that if, for example, you code a managed class in Managed C++, and the class is used in some C# code, any exceptions thrown in the managed C++ class can be caught by the C# code. A major benefit of this is there is no need for checking the HResult for errors any longer (if implemented using exceptions). You just have to code as if things worked correctly, because if they didn't, the error would be caught by the exception handler. I won't go into multilanguage programming in this book, but rest assured it does work.

Basics of Exception Handling

Coding for exceptions is very easy. Basically, you break your code up into three parts: the code for successful execution, the errors, and the code to clean up afterward. In Managed C++, these three parts are known as the try block, the catch block, and the __finally block. You will look at the try and catch blocks now and examine the __finally block at the end of this section.

The process for handling exceptions is a little tricky for new developers because the linear flow of the code is broken. Basically, whenever an error occurs, the program throws an exception. At this point, normal execution flow of the program ends and the program goes in search of a handler for the exception that it threw. You'll see how the program searches for exceptions later in the section "Catching Multiple Exceptions." If it doesn't find an exception, the program terminates. Before Managed C++, this termination would have left programs without cleaning up after themselves, but if you code with managed classes you don't have to worry about this.

Exceptions also have to be thrown within a try block or they will immediately terminate without searching for a handler. The try block is simply a block of code enclosed in curly brackets and prefixed with the keyword try:

try { // code body where exception can be thrown }

After the try block are one or more catch blocks. Each catch block handles a different type of error. A catch block looks similar to a function with one parameter, except that the function name is always catch, there is no return type, and the parameter is the exception type to trap.

catch (<exception-type> el) { // code to handle exception } // repeat for all specific exception types catch (<exception-type> eN) { // generic code to handle exception }

Listing 4-11 shows a simple example of an exception. I noted in Chapter 3 that the __try_cast operator throws a System::InvalidCastException when it is unable to convert from one try to another. This coding example shows how to capture this exception so that it can be handled more elegantly than the abrupt termination that would normally happen.

Listing 4-11: CatchException.exe: Simple Exception Handling Example

#using <mscorlib.dll> using namespace System; __gc class X {}; __gc class Y {}; Int32 main(void) { X *x = new X; try { Y *y = __try_cast<Y*>(x); Console::WriteLine(S"No Exception"); // Should not execute } catch (InvalidCastException *e) { Console::WriteLine(S"Invalid Cast Exception"); Console::WriteLine(e->StackTrace); } return 0; }

Figure 4-2 shows the results of this little program.

Figure 4-2: Results of CatchException.exe

.NET Framework Base Class: Exception Classes

The .NET Framework has an extensive set of exceptions that it may throw. You'll encounter two different types of exceptions while using .NET:

System::ApplicationException is the base class of those exceptions that are user-defined or, in other words, the ones that you have defined yourself.

System::SystemException, on the other hand, handles exceptions created within the CLR, for example, exceptions caused by stream I/O, databases, security, threading, XML, and so on. You can be sure that if the program has aborted due to a system problem you can catch it using the generic System::SystemException.

Both of these exceptions derive from the System::Exception class, which is the root of all .NET exceptions. The System::Exception class provides many useful properties (see Table 4-4) to help resolve any exceptions that might occur.

Table 4-4: Key System::Exception Member Properties

PROPERTY

DESCRIPTION

Helplink

The Uniform Resource Name (URN) or Uniform Resource Locator (URL), if appropriate, to a help file providing more information about the exception.

InnerException

This property gives access to the exception that caused this exception, if any.

Message

A textual description of the error.

Source

The name of the object, assembly, or application that caused the exception.

StackTrace

A text string of all the method calls on the stack made before triggering the exception.

TargetSite

The name of the method that triggered the exception.

SystemException

You can't begin to explore all the exceptions that the .NET Framework class library provides to developers. Even the following illustration, which shows some of the more common exceptions, only shows the tip of the iceberg.

The .NET Framework provides developers with a huge set of classes. Basically, if something could go wrong, the .NET Framework class library provides an exception for it. As you can see from the preceding illustration, the names of the exceptions are pretty self-explanatory, and if you add to them the properties mentioned previously, you have a great tool for finding where your application threw its exception and why.

The best resource to find out about exceptions is the documentation provided by the .NET Framework. You should start your search by looking up System.Exception. From there you should quickly be able to navigate to the exception in question.

There is nothing special about catching exceptions thrown by the system. As long as you place the methods that might throw an exception within a try block, all you have to do is catch the system-thrown exception. Here is about as simple as exception handling comes:

try { // Methods that throw OutOfMemoryException } catch (OutOfMemoryException *oome) // If a method throws an exception { // Execution will continue here // Process exception }

ApplicationException

Truthfully, there is nothing stopping you from throwing exceptions derived from the class System::SystemException or System::Exception. It is even possible to derive an exception from one of the exceptions derived from System::SystemException. The .NET Framework only really added the System::ApplicationException class for readability purposes. In fact, neither System::SystemException nor System::ApplicationException adds any additional functionality to System::Exception.

There is nothing difficult about creating an application exception class. It is just a standard Managed C++ class, but instead of inheriting from System::Object or some other class, you inherit from System::ApplicationException.

_gc class MyException : public ApplicationException { };

Within the custom exception, you can implement anything you want but, in practice, you probably only want to implement things that will help resolve the cause of the exception.

If you are an experienced traditional C++ developer, you know that you could derive your exception from any data type. For example, you could create your exception simply from the System::Object class or even a built-in type such as Int32. This still works in Managed C++ as well, but if you do this you lose the ability to have your exceptions caught by other languages besides Managed C++.

Note

All exceptions you create for your applications should be inherited from System::ApplicationException.

Throwing ApplicationExceptions

Obviously, if you can create your own exceptions, you must be able to throw them too. Technically, you can throw an exception at any time you want, but in practice, it is best only to throw an exception when something in your program fails unexpectedly and normal process flow can no longer continue. The reason for this is that the processing of an exception has a lot of overhead, which can slow the program down when executing. Often, it is better to use if statements to process errors.

Syntactically, the throwing of an exception is very easy. Simply throw a new instance of an exception class. In other words, add code with the following syntax:

throw new <Exception-Class>(<constructor-parameters>);

or, for example:

throw new ApplicationException("Error Message");

If you create your own derived exception, just replace ApplicationException with it and pass any parameters to its constructor—if the construct has any parameters, that is.

The actual throw statement does not have to be physically in the try block. It can be located in any method that gets executed within the try block or any nested method that is called within a try block.

Listing 4-12 shows how to create a custom exception from the .NET Framework's System::ApplicationException. Notice that because you're using the System namespace, you don't have to prefix the exceptions with System::.This program simply loops through the for loop three times, throwing an exception on the second iteration.

Listing 4-12: ThrowDerived.exe: Throwing an Exception

#using <mscorlib.dll> using namespace System; __gc class MyException : public ApplicationException { public: MyException( String *err ) : ApplicationException(err) {} }; Int32 main(void) { for (Int32 i = 0; i < 3; i++) { Console::WriteLine(S"Start Loop"); try { if (i == 0) { Console::WriteLine(S"\tCounter equal to 0"); } else if (i == 1) { throw new MyException(S"\t**Exception** Counter equal to 1"); } else { Console::WriteLine(S"\tCounter greater than 1"); } } catch (MyException *e) { Console::WriteLine(e->Message); } Console::WriteLine(S"End Loop"); } return 0; }

Note that the try block is within the for loop. This is because even though you can resolve an exception and allow code to continue processing, the only place you are allowed to start or resume a try block is from its beginning. So, if the for loop was found within the try block, there would be no way of resuming the loop, even if you used the dreaded goto statement to try to jump into the middle of the try block.

Figure 4-3 shows the results of this little program.

Figure 4-3: Results of ThrowDerived.exe

As you can see, there is nothing spectacular about throwing an exception of your own. It is handled exactly the same way as a system exception, except now you are catching an exception class you created instead of one created by the .NET Framework.

Rethrowing Exceptions and Nested Try Blocks

Sometimes it is possible that your program may catch an exception that it cannot completely resolve. In these cases, the program might want to rethrow the exception so that another catch block can resolve the exception.

To rethrow an exception, simply add this statement within the catch block:

throw;

Once you rethrow the exception, that exact same exception continues to make its way up the stack looking for another catch block that matches the exception. Rethrowing an exception only works with nested try blocks. It will not be caught in a catch block at the same level as it was originally caught and thrown but instead will be caught in a catch block at a higher level.

There is no limit on nesting try blocks. In fact, it is a common practice to have one try block that surrounds the entire program within the main() function and to have multiple try blocks surrounding other areas of the code where an exception has a higher probability of occurring. This format allows the program to catch and resolve exceptions close to where the exception occurred, but it still allows the program to catch other unexpected exceptions before the program ends, so that the program may shut down more gracefully.

Listing 4-13 is a contrived example showing an exception being rethrown within nested try blocks. Of course, nesting try blocks immediately together like this doesn't make much sense.

Listing 4-13: RethrowException.exe: Rethrowing an Exception

#using <mscorlib.dll> using namespace System; Int32 main(void) { try { try { throw new ApplicationException(S"\t***Boom***"); Console::WriteLine(S"Imbedded Try End"); } catch (ApplicationException *ie) { Console::WriteLine(S"Caught Exception "); Console::WriteLine(ie->Message); throw; } Console::WriteLine(S"Outer Try End"); } catch (ApplicationException *oe) { Console::WriteLine(S"Recaught Exception "); Console::WriteLine(oe->Message); } return 0; }

Figure 4-4 shows the results of this little program.

Figure 4-4: Results of RethrowException.exe

Catching Multiple Exceptions

So far, you have only dealt with a single catch block associated with a try block. In reality, you can have as many catch blocks associated with a try block as there are possible exception classes that can be thrown by the try block. (Actually, you can have more, but catching exceptions that are not thrown by the try block is a waste of time and code.)

Using multiple catch blocks can be a little trickier in Managed C++ than in traditional C++ because all exceptions are derived from a single class. As this is the case, the order in which the catch blocks are placed after the try block is important. For catch blocks to work properly in Managed C++, the most-derived class must appear first and the least-derived class or the base class, System::Exception, must appear last.

For example, System::IO::FileNotFoundException must be caught before System:IO::IOException is caught, which in turn must be caught before System::SystemException is caught, which ultimately must be caught before System::Exception. You can find the order of system exception inheritance in the documentation provided by the .NET Framework.

Listing 4-14 shows the correct order of catching exceptions of derived exception class, but this time they are all derived from the System::ApplicationException class. You might want to change the order of the catch blocks to see what happens.

Listing 4-14: MultiException.exe: Catching Multiple Exceptions

#using <mscorlib.dll> using namespace System; __gc class LevelTwoException : public ApplicationException { public: LevelOneException( String *err ) : ApplicationException(err) {} }; __gc class LevelTwoException : public LevelOneException { public: LevelTwoException( String *err ) : LevelOneException(err) {} }; Int32 main(void) { for (Int32 i = 0; i < 4; i++) { Console::WriteLine(S"Start Loop"); try { if (i == 1) throw new ApplicationException(S"\tBase Exception Thrown"); else if (i == 2) throw new LevelOneException(S"\tLevel 1 Exception Thrown"); else if (i == 3) throw new LevelTwoException(S"\tLevel 2 Exception Thrown"); Console::WriteLine(S"\tNo Exception"); } catch (LevelTwoException *e2) { Console::WriteLine(e2->Message); Console::WriteLine(S"\tLevel 2 Exception Caught"); } catch (LevelOneException *el) { Console::WriteLine(e1->Message); Console::WriteLine(S"\tLevel 1 Exception Caught"); } catch (ApplicationException *e) { Console::WriteLine(e->Message); Console::WriteLine(S"\tBase Exception Caught"); } Console::WriteLine(S"End Loop"); } return 0; }

Figure 4-5 shows the results of this little program.

Figure 4-5: Results of MultiException.exe

Catching all Previously Uncaught Exceptions

If you want to correctly code Managed C++ code, which is used in a multilanguage environment, then the easiest way of catching all exceptions is simply to add the catching of System:: Exception to the end of your catch block, because all .NET exceptions—of both system and application origin—are derived from this class.

There is also another way of catching all uncaught exceptions, even those not derived from System::Exception. It is simply a catch block without an exception call. In the class's place is an ellipsis:

catch (...) { }

This form of catch block doesn't provide much in the way of information to help determine what caused the exception, as it doesn't have as a parameter any type of exception to derive from. Thus, there's no way to print out the stack or messages associated with the exception that's generated. All you actually know is that an exception occurred.

In Managed C++, this form of catch block should probably only be used as a last resort or during testing, because if this catch block is executed, your code will not work properly in the .NET portable managed multilanguage environment anyway. Of course, if your code is not destined for such an environment, then you may need to use this form of catch block.

The usual reason that this type of exception occurs in Managed C++ is that the developer forgot to derive his exception class from System::ApplicationException. Listing 4-15 shows exactly this occurring.

Listing 4-15: CatchAll.exe: Catching All Exceptions

#using <mscorlib.dll> using namespace System; __gc class MyDerivedException : public ApplicationException { public: MyDerivedException( String *err ) : ApplicationException(err) {} }; __gc class MyException // Not derived from Exception class { }; Int32 main(void) { for (Int32 i = 0; i < 4; i++) { Console::WriteLine(S"Start Loop"); try { if (i == 1) throw new ApplicationException(S"\tBase Exception"); else if (i == 2) throw new MyDerivedException(S"\tMy Derived Exception"); else if (i == 3) throw new MyException(); Console::WriteLine(S"\tNo Exception"); } catch (Exception *e) { Console::WriteLine(e->Message); } catch (...) { Console::WriteLine(S"\tMy Exception"); } Console::WriteLine(S"End Loop"); } return 0; }

Figure 4-6 shows the results of this little program.

Figure 4-6: Results of CatchAll.exe

Executing Code Regardless of an Exception

There are times when code needs to be run at the completion of a try block, whether the try block completed cleanly or threw an exception. For example, you may want to close a file stream or database that has been open in the try block. Up until now, if you threw an exception, there was no way to ensure that such code was always run unless you put the close statement at the end of each of the try and catch blocks.

With Managed C++, it is now possible to remove this redundant coding by adding a __finally block after the last catch block. The syntax for a __finally block is this:

__finally { // Code to always be executed }

All code within the __finally block will always be executed after the completion of the try block or after the completion of the caught catch block.

As you can see in Listing 4-16, the __finally block is run both at the successful completion of the try block and after the System::ApplicationException catch block is executed.

Listing 4-16: Finally.exe: The Finally Block

#using <mscorlib.dll> using namespace System; Int32 main(void) { for (Int32 i = 0; i < 3; i++) { Console::WriteLine(S"Start Loop"); try { if (i == 0) { Console::WriteLine(S"\tCounter equal to 0"); } else if (i == 1) { throw new ApplicationException(S"\t*Exception* Counter = to 1"); } else { Console::WriteLine(S"\tCounter greater than 1"); } } catch (ApplicationException *e) { Console::WriteLine(e->Message); } __finally { Console::WriteLine(S"\tDone every time"); } Console::WriteLine(S"End Loop"); } return 0; }

Figure 4-7 shows the results of this little program.

Figure 4-7: Results of Finally.exe

Категории