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();
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");
}
}
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();
{
// ...
}
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");
}
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();
}
{
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!");
}
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