Tải bản đầy đủ (.pdf) (26 trang)

Exceptions, Attributes, and Reflection

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (664.28 KB, 26 trang )

259
■ ■ ■
CHAPTER 10
Exceptions, Attributes,
and Reflection
I
n this chapter, you’ll begin by looking at aspects of exception handling in C++/CLI that are
not present in classic C++. Then you’ll look at attributes, which supply metadata for a type and,
although not part of standard C++, may be familiar if you’ve used previous versions of Visual C++.
You’ll learn how to use the existing .NET Framework attributes, examine some of the common
ones, and look at how to define and use your own attributes. Finally, you’ll get a brief overview
of the reflection features of the .NET Framework, which provide a way to discover information
on a type at runtime and use that information to interact dynamically with a type.
Exceptions
Exceptions are supported in classic C++, but not universally used. In .NET Framework
programming, exceptions are ubiquitous, and you cannot code without them. This chapter
assumes you are aware of the basic concepts of exception handling, throwing exceptions, and
the try/catch statement. All of these features of classic C++ are valid in C++/CLI code.
A key difference between exception handling in C++/CLI and in classic C++ is that excep-
tions are always thrown and caught by reference (via a handle), not by value. In classic C++,
exceptions could be thrown by value, which would result in a call to the copy constructor for
the exception object. In C++/CLI, exceptions are always on the managed heap, never the stack.
Therefore, you must use a handle when throwing a C++/CLI exception, as in Listing 10-1.
Listing 10-1. Throwing an Exception
try
{
bool error;
// other code
if (error)
{
throw gcnew Exception();


}
}
Hogenson_705-2C10.fm Page 259 Thursday, October 19, 2006 8:04 AM
260
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
catch( Exception^ exception)
{
// code to handle the exception
}
The Exception Hierarchy
All .NET exceptions inherit from a single root class, System::Exception. Table 10-1 shows some
of the common exceptions thrown by the runtime in C++/CLI code.
What’s in an Exception?
A .NET Framework exception contains useful information that captures information about
what triggered the exception and how to address the problem. For example, the exception source
is stored as a string in the Source property; a text representation of the call stack is included in the
form of the StackTrace property; and there’s a Message property, which contains a message suitable
for display to a user. It’s use is demonstrated in Listing 10-2.
Table 10-1. Some Common .NET Framework Exceptions
Exception Condition
System::AccessViolationException Thrown when an attempt to read or write
protected memory occurs.
System::ArgumentException Thrown when an argument to a method is
not valid.
System::ArithmeticException Thrown when an error occurs in an arithmetic
expression or numeric casting operation. This is
a base class for DivideByZeroException,
NotFiniteNumberException, and

OverflowException.
System::DivideByZeroException Thrown when division by zero occurs.
System::IndexOutOfRangeException Thrown when an array access out of
bounds occurs.
System::InvalidCastException Thrown when a cast fails.
System::NullReferenceException Thrown when a null handle is dereferenced or
used to access a nonexistent object.
System::OutOfMemory Thrown when memory allocation with
gcnew fails.
System::TypeInitializationException Thrown when an exception occurs in a static
constructor but isn’t caught.
Hogenson_705-2C10.fm Page 260 Thursday, October 19, 2006 8:04 AM
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
261
Listing 10-2. Using the Properties of the Exception Class
// exception_properties.cpp
using namespace System;
int main()
{
try
{
bool error = true;
// other code
if (error)
{
throw gcnew Exception("XYZ");
}
}

catch( Exception^ exception)
{
Console::WriteLine("Exception Source property {0}", exception->Source);
Console::WriteLine("Exception StackTrace property {0}",
exception->StackTrace);
Console::WriteLine("Exception Message property {0}", exception->Message);
}
}
The output of Listing 10-2 is as follows:
Exception Source property exception_properties
Exception StackTrace property at main()
Exception Message property XYZ
When an unhandled exception occurs in a console application, the Message and
StackTrace data are printed to the standard error stream, like this:
Unhandled Exception: System.Exception: XYZ
at main()
There’s also a property of the Exception class called InnerException, which may reference
an exception that gives rise to the exception we’re looking at. In this way, a cascading series of
exceptions may be nested one within the other. This could be useful if an exception occurs
deep down in low-level code, but there are several layers of libraries between the problem and
the code that knows how to handle such situations. As a designer of one of the intermediate
libraries, you could choose to wrap that lower exception as an inner exception and throw a
higher exception of a type that is more intelligible to your clients. By passing the inner exception,
Hogenson_705-2C10.fm Page 261 Thursday, October 19, 2006 8:04 AM
262
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
the inner exception information can be used by the error-handling code to respond more
appropriately to the real cause of the error.

Creating Exception Classes
You will often want to create your own exception classes specific to particular error conditions;
however, you should avoid doing this and use one of the standard Exception classes, if possible.
Writing your own exception class lets you filter on and write exception handlers specific to that
error. To do this, you may derive from System::Exception. You would normally override the
Message property in the Exception base class to deliver a more relevant error message (see
Listing 10-3).
Listing 10-3. Creating a Custom Exception
// exceptions_custom.cpp
using namespace System;
ref class MyException : Exception
{
public:
virtual property String^ Message
{
String^ get() override
{
return "You must supply a command-line argument.";
}
}
};
int main(array<String^>^ args)
{
try
{
if (args->Length < 1)
{
throw gcnew MyException();
}
throw gcnew Exception();

}
// The first catch blocks are the specific exceptions that
// you are looking for.
catch (MyException^ e)
{
Console::WriteLine("MyException occurred! " + e->Message);
}
Hogenson_705-2C10.fm Page 262 Thursday, October 19, 2006 8:04 AM
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
263
// You may also catch other exceptions with multiple try blocks,
// although it's better.
catch (Exception^ exception)
{
Console::WriteLine("Unknown exception!");
}
}
The output of Listing 10-3 (with no command-line arguments) is shown here:
MyException occurred! You must supply a command-line argument.
Using the Finally Block
C++/CLI recognizes the finally contextual keyword, which is a feature of other languages that
support exception handling such as Java and C#. The finally keyword precedes a block of code
known as a finally block. Finally blocks appear after catch blocks and execute whether or not an
exception is caught.
Use a finally block (see Listing 10-4) to put any cleanup code that you don’t want to duplicate
in both the try block and the catch blocks. The syntax is like that in other languages.
Listing 10-4. Using a Finally Block
try

{
// ...
}
catch( Exception^ )
{
}
finally
{
Console::WriteLine("finally block!");
}
In the case of multiple finally blocks, they are executed “from the inside out” as demonstrated
in Listing 10-5.
Listing 10-5. Using Multiple Finally Blocks
// multiple_finally_blocks.cpp
using namespace System;
Hogenson_705-2C10.fm Page 263 Thursday, October 19, 2006 8:04 AM
264
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
int main()
{
try
{
Console::WriteLine("Outer try");
try
{
Console::WriteLine("Inner try");
throw gcnew Exception("XYZ");
}

catch( Exception^ exception)
{
Console::WriteLine("Inner catch");
}
finally
{
Console::WriteLine("Inner finally");
}
}
catch(Exception^ exception)
{
Console::WriteLine("Outer catch");
}
finally
{
Console::WriteLine("Outer finally");
}
}
Here is the output of Listing 10-5:
Outer try
Inner try
Inner catch
Inner finally
Outer finally
The first finally block to execute is the one paired with the last try block to execute. The
finally block is a separate scope from the try block, so, for example, any variables declared in
the try block aren’t available in the finally block. Also, if you created any stack objects, their
destructors would be called at the end of the try block and before the finally block executes.
Don’t try to use jump statements (e.g., continue, break, or goto) to move into or out of a
finally block; it is not allowed. Also, you cannot use the return statement from inside a finally

block. If allowed, these constructs would corrupt the stack and return value semantics.
Hogenson_705-2C10.fm Page 264 Thursday, October 19, 2006 8:04 AM
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
265
Dealing with Exceptions in Constructors
A difficult problem in any language is what to do with objects that fail to be constructed prop-
erly. When an exception is thrown in a constructor, the result is a partially constructed object.
This is not a largely theoretical concern; it’s almost always possible for an exception to be thrown in
a constructor. For example, OutOfMemoryException could be thrown during any memory alloca-
tion. The finalizer will run on such partially constructed objects. In C++, destructors do not run
on partially constructed objects. The finalizer is called by the runtime to clean up before the
runtime reclaims the memory. As usual, the execution of the finalizer is nondeterministic, so it
won’t necessarily happen right away, but will happen eventually. This is another reason to write
finalizers carefully, without assuming any objects are valid. In Listing 10-6, an exception is
thrown in the construction of a member of A in A’s constructor. The finalizer is called to clean
up; the destructor is not called.
Listing 10-6. Throwing an Exception in a Constructor
// exceptions_ctor.cpp
using namespace System;
// the type of the member
ref class Class1
{
public:
Class1()
{
// Assume a fatal problem has occurred here.
throw gcnew Exception();
}

};
ref class A
{
Class1^ c1;
Class1^ c2;
public:
A()
{
// c1 has a problem; it throws an exception.
c1 = gcnew Class1();
Hogenson_705-2C10.fm Page 265 Thursday, October 19, 2006 8:04 AM
266
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
// c2 never gets created.
c2 = gcnew Class1();
}
void F() { Console::WriteLine("Testing F"); }
~A() // Never gets called, even if A is created with stack semantics
{
Console::WriteLine("A::~A()");
}
!A() // Gets called for partially constructed object
{
Console::WriteLine("A::!A()");
// Don't try to use C2 here without checking for null first.
}
};
int main()

{
A a;
a.F(); // never reached
}
This example shows what happens in the simple case of a class without a base class other
than Object. In the case where some base classes have already been initialized, the finalizers
for any base classes will also execute.
Throwing Nonexception Types
C++/CLI allows you to throw objects that are not in the exception class hierarchy. If you’ve
done a lot of programming in C# or Visual Basic .NET, this may be somewhat of a surprise,
since in those languages, you are limited to throwing exception objects that derive, directly or
indirectly, from System::Exception. In C++/CLI, you’re not limited in this way. However, if you
are calling C++/CLI code from C# or VB .NET code, and an exception object of an unusual type
is thrown, it will be wrapped in an exception from the point of view of the C# or VB .NET code.
The basic idea is simple, as Listing 10-7 shows.
Listing 10-7. Throwing an Object That’s Not an Exception
// throw_string.cpp
using namespace System;
public ref class R
{
Hogenson_705-2C10.fm Page 266 Thursday, October 19, 2006 8:04 AM
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
267
public:
static void TrySomething()
{
throw gcnew String("Error that throws string!");
}

};
int main()
{
try
{
R::TrySomething();
}
catch(String^ s)
{
Console::WriteLine(s);
}
}
The subtlety arises when you run this C++/CLI code from another language. If the code in
Listing 10-7 is compiled to a DLL assembly and reference in C#, and you call the R::TrySomething
method, a RuntimeWrappedException object is created.
Note that cross-language work is best done in the Visual Studio IDE, so you can be sure
that the right references, assembly signing, and manifests are all done properly. Create two
projects in the same solution (see Listing 10-8). Set the C# project as the startup project, and
configure the C++/CLI project as a DLL. Reference the C++/CLI project from the C# project,
and build.
Listing 10-8. Wrapping a Nonexception Object
// runtimewrappedexception.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.CompilerServices;
class Program
{
static void Main(string[] args)
{

try
{
R.TrySomething();
}
Hogenson_705-2C10.fm Page 267 Thursday, October 19, 2006 8:04 AM
268
CHAPTER 10

EXCEPTIONS, ATTRIBUTES, AND REFLECTION
catch (RuntimeWrappedException e)
{
String s = (String)e.WrappedException;
Console.WriteLine(e.Message);
Console.WriteLine(s);
}
}
}
The output of Listing 10-8 is as follows:
An object that does not derive from System.Exception has been wrapped in a
RuntimeWrappedException.
Error that throws string!
I do not recommend throwing nonexception objects. Throwing exception objects that all
derive from the root of the same exception hierarchy has the advantage in that a catch filter
that takes the Exception class will capture all exceptions. If you throw objects that don’t fit this
scheme, they will pass through those filters. There may be times when that behavior is desired,
but most of the time you are introducing the possibility that your nonstandard exception will
be erroneously missed, which would have undesired consequences. (The paradox is that a
non-Exception exception is an exception to the rule that all exceptions derive from Exception.
You can see how confusing it could be.)
Unsupported Features

Exception specifications are a C++ feature that allow a programmer to declare what exceptions
a particular function can throw. This is intended as a heads-up to users of a function that they
should be prepared to deal with these exceptions. Exception specifications are not supported
in Visual C++ even in native code, and C++/CLI does not support this feature either. In general,
this feature is impractical because it is not usually feasible to list the complete set of exceptions
that a given block of code might generate, most particularly exceptions that propagate from
any function called that doesn’t have exception specifications. Furthermore, some common
exceptions, such as OutOfMemoryException, could be generated almost anywhere. Should these
be included in all exception specifications? Another problem is performance, since this feature
adds to the already intensive runtime overhead associated with exception handling. For all
these reasons, the designers of the CLI chose not to implement this feature.
Exception-Handling Best Practices
Exception handling is controversial. All aspects of exception handling, it seems, are up for
debate. Regardless of what your position is, one thing remains certain: if your framework uses
exceptions, you, too, must use exceptions. For CLI types, there is no option not to use exception
handling. However, you must use it sensibly and with restraint. Exceptions should not be used
in normal flow control, because they do incur a significant performance penalty when thrown
and caught. Exceptions should be used for truly exceptional conditions, errors that would not
be expected from normal, correct program functioning.
Hogenson_705-2C10.fm Page 268 Thursday, October 19, 2006 8:04 AM

×