Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can we determine at runtime if two type_info's would be castable?

Is there a way to determine from two const ::std::type_info objects, let's name them B and D if the type described by D is derived from type B?

I ask because I want to erase the type of an object I get but later on be able to check if it can be safely promoted.

void* data;
const ::std::type_info* D;

template<typename D>
void store(D&& object)
{
    D = &typeid(object);
    data = ::std::addressof(object);
}

template<typename B>
B& load()
{
    // if(typeid(B) != (*D)) throw ::std::bad_cast{};
    return *reinterpret_cast<B*>(data); // <- also problematic
}

I want to be able to use it like this:

class Base {};
class Derived : Base {};

Derived d;
store(d);
// ....
load<Base>();

Thus it is not suitable to just use an equality compare for the typeids. I am pretty sure this could be possible in a similar way that dynamic_cast can figure this out. What I want is that in every case where D& could be assigned to B& allowing B as the type argument of load() - without knowing D at that time.

like image 562
WorldSEnder Avatar asked Sep 28 '22 09:09

WorldSEnder


2 Answers

I found a way to let the compiler and interal mechanism to figure it out for me. I don't have a problem with cross compiling, in that case ::std::type_info isn't consistent, either.

typedef void (*throw_op)(void*);
throw_op dataThrow;

template<typename T>
[[ noreturn ]] void throwing(void* data)
{
    throw static_cast<T*>(data);
}
[[ noreturn ]] void bad_cast()
{
    throw ::std::bad_cast{};
}

template<typename B>
B& poly_load()
{
    if(data == nullptr)
        bad_cast();
    try {
        dataThrow(data);
    } catch (B* ptr) {
        return *ptr;
    } catch (...) {
        bad_cast();
    }
}

All you have to do is add the following line to the store operation:

dataThrow = throwing<D>;

How does this work? It takes advantages of exceptions and how they are caught. Note here that this makes poly_load ?much? slower than the simple load function, thus I'll keep the both around.

C++ says that when an exception of type D* is being thrown you can catch that exception with a catch-clause B* where B is any ancestor of D.

Minimal example:

struct Base {
    virtual ~Base() {}
    virtual void foo() = 0;
};

struct Derived : public virtual Base {
    void foo() override {
        ::std::cout << "Hello from Derived" << ::std::endl;
    }
};

int main() {
    Derived d{};
    store(d);

    // .....

    poly_load<Base>().foo();
}
like image 65
WorldSEnder Avatar answered Oct 06 '22 18:10

WorldSEnder


Actually, you should be using an equality test on your type_info instances.

reinterpret_cast provides no guarantees except when casting back to the exact original type. Even

Derived* d = get_derived();
Base* b = reinterpret_cast<Base*>(d);

will not give a correct result (if the Base subobject is not stored at offset zero within Derived, which is guaranteed only for standard-layout types).

The complete rule is found in section 5.2.10:

An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue v of object pointer type is converted to the object pointer type "pointer to cv T", the result is static_cast< cv T*>(static_cast< cv void*>(v)). Converting a prvalue of type "pointer to T1" to the type "pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value.

Only static_cast and dynamic_cast can perform base subobject adjustments, and those don't kick in when either type is void* (after type erasure).

However, it appears that the Boost developers have worked out all the difficulties. See boost::variant::polymorphic_get

like image 21
Ben Voigt Avatar answered Oct 06 '22 18:10

Ben Voigt