1

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?

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Artem Selivanov
  • 1,867
  • 1
  • 27
  • 45
  • 2
    Upcasting refers to typecasting a child object to a parent object. Downcasting provides casting a parent object to a child object. Casting from void* however is neither. We have to cast exactly to type from what it was cast to void*, regardless if there is RTTI or no. – Öö Tiib Apr 19 '23 at 14:04
  • Why disable RTTI? Yes, there's a small size overhead and (if used) a small runtime overhead. But it's all negligible on modern hardware. – Jesper Juhl Apr 19 '23 at 15:33
  • 3
    Rephrased, your question is can you write code that makes decisions based on the runtime type of an object without compiler-generated runtime type information. Sure; you have to do all the work yourself. That's hard, and error-prone. Why do you want to disable RTTI? – Pete Becker Apr 19 '23 at 15:33
  • Out of curiosity, why is the error type erased in the first place? It isn't in `std::expected` and this type-erasure is the root of your problems. – joergbrech Apr 19 '23 at 15:35
  • @JesperJuhl RTTI may not be recommended in the framework context. For example: In UnrealEngine projects RTTI and Exceptions usually disabled. – Artem Selivanov Apr 20 '23 at 06:13
  • @PeteBecker it is better to say: I don't want to enable RTTI for my project. – Artem Selivanov Apr 20 '23 at 06:15
  • @joergbrech I think this is interesting and good (in my opinion) pattern. Benefits: 1. No additional boilerplate template parameter (Error type); 2. Any error can be setted to this version of `expected` at any time; 3. Error catching interface that brings closer to C++ exceptions – Artem Selivanov Apr 20 '23 at 06:19
  • @ArtemSelivanov -- RTTI is part of the implementation's support for the C++ programming language. If you want to remove parts of the language, you should be able to explain what benefit you gain from that. "may not be recommended in the framework context" is not an explanation. – Pete Becker Apr 20 '23 at 12:18
  • @PeteBecker I think it's about executable size and performance on various platforms. I'm not familiar to each compiler and can't say for sure how rtti can affect on one or other game, but very often every bit matters, especially on non-desktop platforms (ps4). Also ue have own reflection system, it doesn't need rtti – Artem Selivanov Apr 20 '23 at 14:33

1 Answers1

0

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();
}

Artem Selivanov
  • 1,867
  • 1
  • 27
  • 45