< C++ .Net Early Stages 9 | Main | C++ .Net Early Stages 11 >


 

 

Early Stages of the C++ .Net 10

(Managed Extensions for C++)

 

 

 

Note: The code snippets used in this module are for Visual C++ .Net 2003 and if compiled using Visual C++ .Net 2005 you need to use the /clr:oldSyntax option). Here you will learn new keywords used in the C++ .Net though in VC++ 2005 these keywords already deprecated. However we just want to see how the whole thing changed to the managed C++ .Net from native, unmanaged C++ used in COM, MFC etc.

  1. Exceptions and Managed Code

  2. Unmanaged Exceptions

 

Exceptions and Managed Code

 

.NET finally removes the need for old-style C error reporting mechanisms. C code typically used function return values to report errors, and even static variables to hold the last reported error with the associated problem of maintaining a list of what each error value means. This system persisted into C++ through COM programming, where HRESULTs were the main error-reporting mechanism. In .NET, method return values are just that they are intended to return results. Errors are reported through exceptions. All .NET exceptions should be instances of System::Exception or derived classes; there is no concept of throwing a primitive type or throwing a C++ reference. System::Exception looks like this:

[Serializable]

__gc class Exception : public ISerializable

{

public:

   Exception();

   Exception(String*);

   Exception(String*, Exception*);

   Exception(SerializationInfo*, StreamingContext*);

   virtual String* get_HelpLink();

   virtual void set_HelpLink(String*);

protected:

   int get_HResult();

   void set_HResult(int);

public:

   virtual Exception* get_InnerException();

   virtual String* get_Message();

   virtual String* get_Source();

   virtual String* get_StackTrace();

   virtual Exception* GetBaseException();

   virtual void GetObjectData(SerializationInfo* info, StreamingContext* ctx);

};

 

 

 

 

 

You have seen ISerializable. Because Exception implements ISerializable (and has an appropriate constructor), exceptions can be serialized. When you call a remote object via .NET remoting and that object throws an exception, the exception is serialized and passed via the remoting channel to the calling code, where it is re-thrown. Exceptions can be created based on other exceptions. This arrangement means that if your code catches an exception that it cannot handle, it can create a new exception based on the exception it has caught and add its own description string. This system allows code to build up a list of exceptions, each of which can be accessed through the InnerException property.

// Get Exception* e from somewhere.

while (e != 0)

{

   Console::WriteLine(e->Message);

   e = e->InnerException;

}

Managed C++ uses the same keywords as native C++ to guard code that can generate exceptions.

StreamReader* txt;

try

{

   txt = File::OpenText(S"file.txt");

}

catch(FileNotFoundException* fnfe)

{

   Console::WriteLine(S"cannot find file");

   throw new Exception(S"cannot process file", fnfe);

}

catch(Exception* e)

{

   Console::WriteLine(S"some other reason");

   throw;

}

If the file data.txt does not exist, a FileNotFoundException will be thrown. This exception is caught by the first catch clause, which prints out a diagnostic message and then throws a new exception based on the caught exception. The second catch clause behaves like the unmanaged C++ catch(...) to catch all exceptions not caught by the earlier catch clause. Clearly, you should arrange exception handling so that the more generic exceptions are caught lower down in the list of handlers, and the C++ compiler will warn you if you catch an exception type that is higher in the inheritance tree than other exceptions caught lower in the catch handler list. Managed C++ allows you to use exception specification on methods, but there is no concept of exception specifications in .NET, so they are ignored. However, we find exception specifications a great documentation device.

__gc class Test()

{

public:

   Test() throw()

   {}

   void f(int x) throw(ArgumentException*)

    {

      if (x < 0)

         throw new ArgumentException(S"x must be >= 0");

      // Use x.

    }

};

Earlier we deliberately ignored the issue of throwing __value types as exceptions. You can throw __value types as long as you box the value first, as shown here:

try

{

   throw __box(42);

}

catch(Object* o)

{   }

Notice that to catch the exception, the catch handler catches an Object* pointer. We do not like this code because the exception is not based on System::Exception, which means that you lose the ability to nest exceptions. Another drawback is that because most code will have an exception handler for Exception*, most code will miss your boxed __value type exception, which means that your exception will propagate up the stack. It is best not to throw exceptions like this. Constructors cannot return values, so if a constructor does not succeed; the only way that you can inform the caller is to throw an exception. If you throw an exception from the constructor of a managed class, the instance will not be created. Thus, the caller will get a null pointer. It is important that if a class’s constructor could throw an exception, you check the pointer returned to make sure that it is not null.

FileInfo* info;

try

{

   info = new FileInfo(strFileName);

}

catch(Exception*)

{

   // Handle the exception here...

}

if (info != 0)

{

   Console::WriteLine(S"{0} is {1} bytes", info->Name, __box(info->Length));

}

In this code, information about a file strFileName can be obtained through a System::IO::FileInfo object. If the variable strFileName is the empty string, the constructor of this class will throw an ArgumentException exception and the info variable will be zero. Once caught in a catch handler, the exception will not propagate further. To propagate the exception outside of this guarded block, you need to re-throw it, using either a new exception or the same one.

StreamReader* txt;

try

{

   txt = File::OpenText(S"file.txt");

}

catch(Exception* e)

{

   Console::WriteLine(S"some other reason");

   throw;

}

The handler in this example prints out a diagnostic message, but because it does not want to handle the exception, the code re-throws the exception by calling throw with no parameter. Managed C++ does not support re-throwing an exception outside of a catch handler, and if you try to do so, the exception will be treated as a generic exception and will be caught as an SEHException.

void ThrowException()

{

   throw;

}

 

void Test(Object* o)

{

   try

   {

      try

      {

         if (o == 0)

            throw new NullReferenceException;

         // Use o here.

      }

      catch(Exception*)

      {

         ThrowException();

      }

   }

   catch(SEHException* e)

   {   }

   catch(NullReferenceException* e)

   {   }

}

In this code, a separate method is used to re-throw the exception. However, although the exception would appear to be a NullReferenceException, it will actually be thrown as an SEHException. If the exception is re-thrown from the original catch handler, it will be re-thrown as the original type NullReference­Exception. It is usually a good idea to re-throw exceptions if the method returns values. The reason can be seen in the following code:

void Errant(int& i)

{

   i = 99;

   throw new Exception("ignore my results");

}

 

void CallErrant()

{

   int j = 0;

   try

   {

      Errant(j);

   }

   catch (Exception*)

   {   }

   Console::WriteLine("j has the value {0}", __box(j));

}

The code calling Errant passes a parameter by reference. The method initializes this reference and then throws an exception. Because Errant has thrown an exception, any results from this method are suspect, something exceptional has happened. This code is bad because it catches the exception and then attempts to use the results. You can also define a finally handler. This is code that is called whenever code guarded by try is left, regardless of whether this is due to an exception, a call to return from the method, or the end of the try block being reached. Managed C++ reuses the __finally keyword from Win32 structured exception handling.

StreamReader* txt;

try

{

   txt = File::OpenText(S"file.txt");

   Console::WriteLine(txt->ReadToEnd());

}

catch(FileNotFoundException* fnfe)

{

   Console::WriteLine(S"cannot find file");

   throw new Exception(S"cannot process file", fnfe);

}

catch(Exception* e)

{

   Console::WriteLine(S"some other reason");

   throw;

}

__finally

{

   if (txt != 0) txt->Close();

}

This code ensures that the file is closed when it is no longer being used. If an exception is thrown during the call to OpenText, the txt reference will be zero but the __finally clause will still be executed, hence the reason for the check on this pointer. If the file is successfully opened but an exception is thrown when reading the file, the catch handler will be executed first before the __finally clause is executed. If the file is successfully opened and the call to read from the file succeeds, __finally is still called. This means that the file is always closed correctly. The alternative is to call Close outside of the guarded block, but this arrangement means that if an exception is thrown, Close will not be called (because the exception handlers re-throw the exception). Instead, the reference to the object will be lost when the stack frame is unwound, and eventually the garbage collector will do a collection, which will result in a call to the Finaliser on the StreamReader object, which will eventually call Close.

This behavior of the stack unwinding differs from unmanaged C++. If you have stack instances of __nogc classes, the destructors of these objects will be called when the stack frame is unwound. .NET objects do not really have destructors, so although the local reference to the object will disappear when the stack is unwound, the objects created by code in that stack frame will not necessarily be destroyed. This destruction occurs when the garbage collector decides to perform garbage collection. You can specify a generic function to catch unhandled unmanaged exceptions with the _set_se_translator method. .NET has an equivalent of this method through the AppDomain::UnhandledException event. As the name suggests, this event will catch any exception thrown from any thread that is running in the application domain that has not been caught. The event is really a case of post-mortem handling. There is little chance of allowing your process to continue running, but it does at least allow you to prevent the standard exception handling from generating a dialog that might frighten your users. If an exception is thrown by the main thread of your process, it will be passed to the Just-In-Time (JIT) debugger before being passed to the unhandled exception handler.

 

Unmanaged Exceptions

 

If you call unmanaged code, for example, through platform invoke (PInvoke), that code might throw an exception. This exception will be propagated as a native structured exception. Your code can catch these exceptions. The .NET Framework tests the type of exception that is thrown and attempts to create a suitable .NET Framework exception object. If there is not a suitable .NET Framework exception, SEHException is used. This exception class does very little work for you. It does have a member named ErrorCode, but this merely returns 0x80004004 (HRESULT E_FAIL). You can call Marshal::GetExceptionCode, which will return the SEH exception code, and Marshal::GetExceptionPointers to get the Win32 EXCEPTION_POINTERS structure, which enables you to determine the code that threw the exception and the state of the CPU registers. Be aware that this method returns an IntPtr, so you are responsible for extracting the information out of an unmanaged structure. Your code can throw and catch native C++ exceptions.

try

{

   f();

   throw 42;

}

catch(int x)

{  }

catch(Exception* e)

{  }

The compiler will generate both .NET and native C++ handling for this code. If method f is managed and throws an exception, the exception will be caught by the final exception handler. If f does not throw an exception, the native exception will be thrown (through a method named _CxxThrowException that the compiler adds to your assembly). By default, the /GX- switch is used when the compiler is invoked from the command line that is, unwind semantics are not enabled, so code such as this example will have to use one of the /EH switches. Because the Standard Template Library (STL) can catch and throw exceptions, you must use one of the /EH switches if you use STL in your unmanaged code.

 

 

 

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

 


 

< C++ .Net Early Stages 9 | Main | C++ .Net Early Stages 11 >