C++ user-defined exception classes
Introduction
Due to the fact that C++ allows to allocate an exception on stack, when in C# language exception is a reference type, it was necessary to design a solution which allows to extend exception instance lifetime and preserve its type without losing an opportunity to use exception in the common for C++ way.
Problem description
Due to the fact that C# exceptions are represented as object type it can be stored or caught with exception type trimming. Let see sample code:
// C# code
Exception e = null;
try
{
throw new ArgumentException("abc");
}
catch (Exception caught)
{
e = caught; // (1)
}
try
{
throw e; // (2)
}
catch (ArgumentException arg)
{
Console.WriteLine("Caught!");
}
If the most common approach for C++ language, when exception instance is stack-allocated, is used thrown exception from the second try catch block won’t be caught. To solve this exception type trimming new approach was designed.
Approach description
In order to accomplish defined goals C# exception classes were splitted in 2 instances.
First one, from this point and below lets call it exception body, mostly serves utility functions. It defines exception class functions, contains logic and methods of C# exception classes (such as ToString() and Equals()) as well as user-defined logic. The names of such classes are usually prefixed with “Details_” string. Such classes inheritance represents C# exception class hierarchy, instances of such classes are always allocated on heap and are responsible for exception data owning, RTTI, instances comparison and so on. Derived exception body is always inherited from base exception body. All such classes must be inherited from System::Details_Exception directly or indirectly. It is prohibited to throw instances of exception body. It is also prohibited to create instances of exception body by calling its constructor.
Second part is ExceptionWrapper template class and its instantiations. From architecture point of view, this classes only contain shared pointer to exception body and provide indirect access to it via calls of pointer dereferencing operator. Also, ExceptionWrapper template allows to make upcast and downcast of exception without exception type trimming. Inheritance tree is the same as for exception bodies, but it is not required to specialize ExceptionWrapper template for each class, as all required inheritance is being generated by the default body. Instances of ExceptionWrapper must be stack-allocated only. Also, only instances of ExceptionWrapper template can be used for throw syntax constructions. ExceptionWrapper class is also inherited from std::exception, which allows to use it in C++ notation.
As an example, lets define new exception class UnknownValueException, which is inherited from System.ArgumentException, which is in turn inherited from System.Exception. When the developer wants to correctly define such exception, they need to define Details_UnknownValueException class, which is inherited from System::Details_ArgumentException, which is already inherited from System::Details_Exception. In the exception body, one must provide additional RTTI information (please, see Requiremens section below). ExceptionWrapper template class will automatically create necessary inheritance tree by using SFINAE approach. From this point, ExceptionWrapper<Details_UnknownValueException> is inherited from ExceptionWrapper<System::Details_ArgumentException>, which is already inherited from ExceptionWrapper<System::Details_Exception>.
It is not allowed to create instances of exception body manually. It is only allowed to create ExceptionWrapper instances, which will create and own instances of required exception bodies. To throw exception, ExceptionWrapper::Throw() method must be called. Throwing ExceptionWrapper instances directly is not recommended, as the exception type will be trimmed to the one being thrown. Using ExceptionWrapper::Throw guarantees, that the type of contained exception body will be rethrown, even if the ExceptionWrapper instance was type-trimmed.
Declaring compatible exceptions
There are several macros available to simplify declaring the compatible excecption types. When there’s a single module involved, the compact declaration may be done at a single place. When the exception instances are to cross module border, one may use the syntax to define the exception class and its members separately, which also allows for neccessary export macros.
All neccessary macros are defined at <system/exception.h> header in the ‘include’ subdirectory of CodePorting.Native Cs2Cpp package.
Compact definition
There are 3 macros to use when defining custom exception type in-place:
- CODEPORTING_USER_EXCEPTION_BEGIN which starts the exception class declaration with neccessary internal members. It also defines the required ExceptionWrapper specialization. After this macro, you’re effectively inside the exception body class and may define constructors and other members. This macro takes the following arguments:
- Full namespace the class is being defined into (e. g. ‘Living::FoodControl’);
- Name of the exception class (without ‘Details_’ prefix, e. g. ‘MealMissed’);
- Name of the parent exception body class (with ‘Details_’ prefix, e. g. ‘System::Details_Exception’).
- CODEPORTING_USER_EXCEPTION_CONSTRUCTOR which starts the exception constructor definition. Please note that this is the only valid way of adding constructors to exception classes (but the members of other kinds can be added normally). Constructor body must follow this macro. All constructors defined this way are protected which is the intended as only the ExceptionWrapper class is allowed to instantiate exception bodies. This macro takes the following arguments:
- Name of the exception class (without ‘Details_’ prefix);
- Arguments with types, wrapped into CODEPORTING_ARGS macro (e. g. ‘CODEPORTING_ARGS(String mealName, bool hungry)’ or ‘CODEPORTING_ARGS()’ for default constructor);
- Arguments without types, wrapped into CODEPORTING_ARGS macro (e. g. ‘CODEPORTING_ARGS(mealName, hungry)’ or ‘CODEPORTING_ARGS()).
- CODEPORTING_USER_EXCEPTION_END which closes the exception class declaration.
The below example defines two exception classes, MealMissed and DinnerMissed, the later being the subclass and the former being the superclass. The code that follows illustrates that the aforementioned problem is solved.
#include <system/exceptions.h>
using namespace System;
namespace Living
{
namespace FoodControl
{
CODEPORTING_USER_EXCEPTION_BEGIN(Living::FoodControl, MealMissed, System::Details_Exception)
CODEPORTING_USER_EXCEPTION_CONSTRUCTOR(MealMissed, CODEPORTING_ARGS(String mealName, bool hungry), CODEPORTING_ARGS(mealName, hungry))
: ::System::Details_Exception(u"Missed a meal: " + mealName), m_hungry(hungry)
{}
public:
bool isHungry() const
{
return m_hungry;
}
private:
bool m_hungry;
CODEPORTING_USER_EXCEPTION_END
CODEPORTING_USER_EXCEPTION_BEGIN(Living::FoodControl, DinnerMissed, Details_MealMissed)
CODEPORTING_USER_EXCEPTION_CONSTRUCTOR(DinnerMissed, CODEPORTING_ARGS(), CODEPORTING_ARGS())
: Details_MealMissed(u"dinner", true)
{}
CODEPORTING_USER_EXCEPTION_END
}
}
TEST(ExceptionsTest, UserDefinedExceptionTest)
{
Living::FoodControl::MealMissed exception(nullptr);
try
{
throw Living::FoodControl::DinnerMissed();
}
catch (const Living::FoodControl::MealMissed &caught)
{
exception = caught;
}
try
{
exception.Throw(); // Unlike 'throw exception', won't trim the type
FAIL();
}
catch (const Living::FoodControl::DinnerMissed &caught)
{
ASSERT_TRUE(caught->isHungry());
ASSERT_EQ(caught->get_Message(), u"Missed a meal: dinner");
}
catch (const Living::FoodControl::MealMissed&)
{
FAIL();
}
catch (...)
{
FAIL();
}
}
Cross-module exceptions
There are the macros to use when defining custom exception type that is capable of crossing module’s borders.
- CODEPORTING_DECLARE_USER_EXCEPTION_BEGIN which starts the exported exception class declaration with neccessary internal members. It also defines the required ExceptionWrapper specialization. After this macro, you’re effectively inside the exception body class and may define constructors and other members. This macro takes the following arguments:
- Export/import macro for the class level (e. g. ‘attribute((visibility(“default”)))');
- Export/import macro for the method level (e. g. ‘__declspec(dllexport)');
- Full namespace the class is being defined into (e. g. ‘Living::FoodControl’);
- Name of the exception class (without ‘Details_’ prefix, e. g. ‘MealMissed’);
- Name of the parent exception body class (with ‘Details_’ prefix, e. g. ‘System::Details_Exception’).
- CODEPORTING_EXPORTED_USER_EXCEPTION_CONSTRUCTOR which makes the exception constructor declaration. Please note that this is the only valid way of adding constructors to exception classes (but the members of other kinds can be added normally). Constructor body must be defined separately. All constructors defined this way are protected which is the intended as only the ExceptionWrapper class is allowed to instantiate exception bodies. This macro takes the following arguments:
- Export/import macro for the method level (e. g. ‘__declspec(dllimport)');
- Name of the exception class (without ‘Details_’ prefix);
- Arguments with types, wrapped into CODEPORTING_ARGS macro (e. g. ‘CODEPORTING_ARGS(String mealName, bool hungry)’ or ‘CODEPORTING_ARGS()’ for default constructor);
- Arguments without types, wrapped into CODEPORTING_ARGS macro (e. g. ‘CODEPORTING_ARGS(mealName, hungry)’ or ‘CODEPORTING_ARGS()).
- CODEPORTING_USER_EXCEPTION_END which closes the exception class declaration.
- CODEPORTING_USER_EXCEPTION_IMPLEMENTATION which defines all internal members of the exception class. Unlike all other macros, this should only be used in a ‘cpp’ file.
The below example defines the exception class in an exportable-importable way.
// api_defs.h
#pragma once
#if defined(_MSC_VER)
#if defined(MY_MODULE_SHARED_EXPORTS)
#define MY_MODULE_SHARED_API __declspec(dllexport)
#else
#define MY_MODULE_SHARED_API __declspec(dllimport)
#endif
#define MY_MODULE_SHARED_CLASS
#elif defined(__GNUC__)
#if defined(MY_MODULE_SHARED_EXPORTS)
#define MY_MODULE_SHARED_API __attribute__((visibility("default")))
#define MY_MODULE_SHARED_CLASS __attribute__((visibility("default")))
#else
#define MY_MODULE_SHARED_API
#define MY_MODULE_SHARED_CLASS
#endif
#else
#define MY_MODULE_SHARED_CLASS
#define MY_MODULE_SHARED_API
#endif
// cross_module_exception.h
#pragma once
#include "api_defs.h"
#include <system/exceptions.h>
namespace my_module
{
enum class ThrownFrom
{
ProjectA,
ProjectB,
ProjectC
};
CODEPORTING_DECLARE_USER_EXCEPTION_BEGIN(MY_MODULE_SHARED_CLASS, MY_MODULE_SHARED_API, my_module, CrossModuleException, ::System::Details_Exception)
CODEPORTING_EXPORTED_USER_EXCEPTION_CONSTRUCTOR(MY_MODULE_SHARED_API, CrossModuleException, CODEPORTING_ARGS(ThrownFrom thrown_from), CODEPORTING_ARGS(thrown_from))
public:
MY_MODULE_SHARED_API ThrownFrom WhereThrown() const;
private:
ThrownFrom m_thrown_from;
CODEPORTING_USER_EXCEPTION_END
}
// cross_module_exception.cpp
#include "cross_module_exception.h"
CODEPORTING_USER_EXCEPTION_IMPLEMENTATION(MY_MODULE_SHARED_CLASS, MY_MODULE_SHARED_API, my_module, CrossModuleException, ::System::Details_Exception)
my_module::Details_CrossModuleException::Details_CrossModuleException(ThrownFrom thrown_from)
: System::Details_Exception(u"CrossModule"), m_thrown_from(thrown_from)
{}
my_module::ThrownFrom my_module::Details_CrossModuleException::WhereThrown() const
{
return m_thrown_from;
}