As you're working in an embedded environment, you presumably favour extremely minimal solutions and you can take advantage of non-standard or non-portable facts about your compiler.
If a class is polymorphic (has at least one virtual function of its own) in C++, it probably has a pointer to a vtable embedded somewhere in it. It may be that the vtable pointer appears at the beginning of the object's layout in memory.
This is true of many compilers, ones that use the C++ ABI - a related SO question here.
If so, you might be able to get at the vtable like this:
void *get_vtable(void *obj)
{
return *(reinterpret_cast<void **>(obj));
}
Then you can compare the vtables of two pointers-to-objects to see if they point to the same type of object.
So a "type switch" (which is what catch basically is) would do something like this:
P p;
Q q;
if (get_vtable(caught) == get_vtable(&p))
{
// it's a P...
}
else if (get_vtable(caught) == get_vtable(&q))
{
// it's a Q...
}
You could hide that pattern in a CATCH macro.
Important point - if you derive a class from a base, but the derived class does not override any virtual functions or add any new virtual functions, then the compiler could conceivably reuse the base class's vtable for the derived class. This means that in order to distinguish between two exception types, they must each override a virtual function, to ensure that they have their own vtables.
Note that this is only a tiny fraction of what exception handling involves. There is also the small matter of unwinding the stack! You need to call the destructors of all objects on the stack when you jump to the handler. It's not just a matter of doing setjmp/longjmp.