4

In C++ you can pass instances of Derived to functions accepting Base, if Base is a base class of Derived. This is very useful for satisfying APIs and a very common design pattern for APIs.

Currently I am faced with a situation, where I want to upcast through an std::any. That is I have an std::any that stores an instance of Derived and I would like to cast its address to a pointer to Base in an API function, that should not know of the existence of Derived. My use case is a runtime reflection library that passes around std::anys storing instances of the reflected types.

I understand that upcasting through std::any is not possible, because std::any_cast checks the typeid before casting and returns a nullptr if the types don't match, see cppreference.

But maybe there is a workaround or some clever trick that I could use? It feels like it must be possible somehow, as upcasting is such a common thing in C++ and the std::any has the address and type I need.

Here is a code example. The following code segfaults as expected because the std::any_cast returns a nullptr in any_function which is dereferenced in the following line. Compiler explorer: https://godbolt.org/z/E9sG9G3ff

#include <any>
#include <iostream>

class Base 
{
public:
    double get_val() const {
        return val;
    }

private: 
    double val {1.23};
};

void normal_function(Base const& b){
    std::cout << b.get_val() << std::endl;
};

void any_function(std::any const& b){
    auto* ptr = std::any_cast<Base>(&b);
    std::cout << ptr->get_val() << std::endl;
}

class Derived : public Base {};

int main()
{
    Derived d;
    
    // normal upcasting

    normal_function(d);

    // upcasting thru std::any

    std::any ad = d;
    any_function(ad);
    
    return 0;
}
joergbrech
  • 2,056
  • 1
  • 5
  • 17
  • 3
    Problem is, casting from `Derived*` to `Base*` might need pointer arithmetics, which depend on where `Base` is in `Derived`'s layout. So, at the point where you do the cast, you must be aware of both types. For polymorphism among 'normal' classes, it works as caller is aware that a cast is necessary, but for `std::any`, no such mechanism exists. Are you bound to use `std::any` or can you use other types? – lorro Jul 19 '22 at 21:18
  • 1
    Nope, this is not possible. You should investigate whether `std::any` is even needed in the first place. It's often used as a cop-out, a quick and dirty hack, instead of implementing a proper, fully type-safe code. Perhaps a more proper solution is possible that avoids the use of `std::any`. – Sam Varshavchik Jul 19 '22 at 21:18
  • Thanks for the quick replies. That's what I was afraid of. Basically I need to pass around objects that can store any arbitrary type, and since I like type-safety and the stl, I went for `std::any` rather than something along the lines of `void*` ... but maybe `std::any` is too limiting and I have to come up with my own non-stl any-like container class. – joergbrech Jul 19 '22 at 21:23
  • 1
    @joergbrech: `void*` can't do what you're trying to do either. You cannot cast a `Derived*` to a `void*` and then cast that to a `Base*`. – Nicol Bolas Jul 19 '22 at 21:26
  • @joergbrech: `std::any` is virtually never the solution to any problem. In most cases, when you want "any arbitrary type", what you really want is `std::variant`. If `std::variant` doesn't fit your needs, then you've designed something that doesn't work, and need to go back to the drawing board. – Mooing Duck Jul 19 '22 at 21:43
  • I want to build something along the lines of http://www.lucadavidian.com/2021/06/21/simple-runtime-reflection-in-c/ for a very generic plugin system. I figured that the custom `Any` class was due to lack of C++17, but it's dawning on me now why it is needed. With `std::any` I would not be able to reflect data members and functions of base classes. I believe this is a use case, where you either need a dynamically typed language or true type erasure. A variant wouldn't be very helpful, because there is no way I can know all possible types in advance. – joergbrech Jul 20 '22 at 05:14

1 Answers1

7

any is a type-safe void*. That means it obeys the rules of a void*. Namely, if you cast a T* to a void*, the only type you can cast it back to is T*. Not to a U* where U is a base class of T. It must be exactly and only T*.

The same goes for any.

any (like this use of void*) is for situations where code A needs to communicate with code C, through some intermediary B. A and C agree on what type is involved, but B doesn't need to know what type is being communicated (perhaps because B facilitates transactions between many different pairs of As and Cs).

Agreeing on what type is involved means the exact type that is involved. If A is communicating with code that only takes a base class, then that's what it needs to provide (by storing a pointer to the base class of its derived class instance).

Now, the general idea of what you're asking for is not unreasonable. However, implementing it is... difficult. Most forms of type erasure can handle types that have similar behavior, but the behavior of the type being erased needs to be known at the time of the erasure, not the time of use of that functionality. If a type needs to be convertible to some type, then exactly what that type is needs to be known when the object is added to the any. That would become part of the interface of the hypothetical any type, and thus it would be baked into its code.

It cannot take "any" type and then later ask, "hey, are you convertible to ThisType?"

Passer By
  • 19,325
  • 6
  • 49
  • 96
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982