Is that possible to upcast erased type without RTTI?

156 Views Asked by At

I have a custom implementation of std::expected, but with a type-erased error. It makes the expected type looks similar to exceptions. We just can use the next code:

Expected<int> V = 123;
V.SetError(std::string("Error occurred"));

And when we try to get the value from Expected, we can just output the stored value to screen (if it supports some function overloading for std::string or another type that is stored in Expected).

I think this is good practice if we don't want to use exceptions.

But, it would also be nice to handle the error by its parent class, similar to a try/catch:

struct MathError{};
struct ZeroDivisionError : MathError{};

Expected V = 321;
V.SetError(ZeroDivisionError{});

if (auto Error = V.Catch<MathError>())
{
   // we go here if error is subclass of MathError
}

But, I have disabled RTTI in my project, and now I can't use dynamic_cast to upcast the stored error (as void*) to MathError*.

Ok, I can make an individual type identifier for each type that is used as an error using a static template function (1 instantiated type = 1 type id).

But, in this case, I can only cast to the exact type if their ids are the same. Maybe something like std::bases would help me (I could enumerate all the classes and make a list of their ids), but this feature is not available in the standard.

So, is there any chance?

1

There are 1 best solutions below

0
On

I found good solution to solve it without RTTI

We can declare class that can be caught by using such expected interface.

First, declare the base class of RuntimeError and add static function that returns type id of itself.

class RuntimeError
{
    static set<size_t> GetBaseIds()
    {
        return { TypeId::GetTypeId<RuntimeError>() };
    }
}

Next, create a template class that will add some stuff to intermediate parent:

template<typename Parent>
class DeriveError : Parent
{
   static set<size_t> GetBaseIds()
   {
      set<size_t> BaseIds = Parent::GetBaseIds();
      BaseIds.insert(TypeId::GetTypeId<Parent>());
      return BaseIds ;
   }
}

Now we can derive error from RuntimeError easily.

class MathError : DeriveError<RuntimeError> {};
class ZeroDivisionError : DeriveError<MathError> {};

Now, when we set error to expected, we can send all ids of each error parent class:

    template<typename E>
    void SetError(E Error)
    {
        // Erased error handler
        ErrorHandler = make_unique<ErrorHandler<E>>(Error);

        if constexpr (is_base_of_v<RuntimeError, E>)
        {
            set<size_t> Bases = E::GetBaseIds();
            Bases.Add(TypeId::GetTypeId<E>());
            // Send parent ids to error handler
            ErrorHandler->SetBases(Bases);
        }
    }

Catch method:

    // avoid catching if not derived from RuntimeError
    template<typename E>
    typename enable_if<is_base_of_v<RuntimeError, E>, const E*>::type
    Catch()
    {
        const int32 ErrorTypeId = TypeId::GetTypeId<E>();
        // Error handler can return NULL if ErrorTypeId not matches any type id
        return static_cast<const E*>(ErrorHandler->TryCatchErrorByTypeId(ErrorTypeId));
    }

Now we can set and catch errors:

Expected Value = 1234;
Value.SetError(ZeroDivisionError>());

if (auto Error = Value.Catch<RuntimeError>())
{
    cout << "Error caught: " << Error.ToString();
}