0

I want to create a vector of "types", then check if an instance of an object is or inherits from any of the types inside this vector.

In C#, I can achieve this result with this code:

public bool IsOrInheritsFrom(SomeClass instance, Type type)
{
    Type instanceType = instance.GetType();
    return instanceType == type || instanceType .IsSubclassOf(type);
}

I have spent hours trying to reproduce this in C++ without success. All the other answers on Stackoverflow are only valid if you already know the type (using dynamic_cast), or if you want to compare two instances (with std::decay<decltype>).

Is it possible at all?

My goal is to have a flexible way to check if an instance belongs to one of the classes provided by "the client" of my code. The list of validTypes would be the part that needs to be flexible.

Example of what I want to achieve with pseudo code:

vector<Types> validTypes;
validTypes.push_back(Dog);
validTypes.push_back(Cat);
validTypes.push_back(Hamster);

Labrador* somePet = new Labrador();

for(Type t : validTypes ){
    // Returns true because Labrador inherits from Dog
    if( isOrInheritsFrom(somePet, t) ){
        return true;
    }
}


Ryan Pergent
  • 4,432
  • 3
  • 36
  • 78
  • It is getting downvoted already for "lack of details", but I don't know what I could add more than literally the code of what I want to achieve. It's hard to make it more specific than that. – Ryan Pergent Jun 10 '23 at 20:04
  • Even for C# this smells like bad code. For C++ there's little builtin support for this kind of logic, but you could probably come up with code that works similar to this. Probably better to ask yourself first, if there's a way rewriting the logic using this function to avoid the use of this function being necessary. – fabian Jun 10 '23 at 20:13
  • 1
    XY problem. Why do you need this bit of information? – n. m. could be an AI Jun 10 '23 at 20:15
  • The problem starts here: "_I want to create a vector of "types",..."_. A C++ `std::vector` can only hold 1 type. So if you could clarify that it would help. – Richard Critten Jun 10 '23 at 20:15
  • @n.m. Because I want to check if an instance belongs to a predefined list of valid classes. – Ryan Pergent Jun 10 '23 at 20:23
  • @fabian That's very judgmental. I am not sure why this "smells of bad code" when it does *exactly* what I need it to do. "come up with code that works similar to this" that's what I tried to do for hours before coming here... To apparently get downvoted because I didn't figure it out on my own. – Ryan Pergent Jun 10 '23 at 20:24
  • 1
    I am not sure how to do this, but meta programming using a [`std::tuple`](https://en.cppreference.com/w/cpp/utility/tuple) of types and [`std::derived_from`](https://en.cppreference.com/w/cpp/concepts/derived_from) is where I would start. – Richard Critten Jun 10 '23 at 20:24
  • @RichardCritten I don't want to make a vector of several types. I want to make a vector that stores types. It is something that is doable in C#. I am not sure how to describe it, but it's basically a vector that stores class names instead of class instances. – Ryan Pergent Jun 10 '23 at 20:26
  • 1
    `std::vector` can't do that as C++ does not have true runtime reflection. Any use of the type system needs to be done at compile time probably using template meta-programming. – Richard Critten Jun 10 '23 at 20:27
  • What do you envision the prototype of this function would look like? How do you expect to call it? How would you even end up with an instance of an object you don't know the type of - are you passing `void*` around? I second the opinion that this looks like an [XY problem](https://xyproblem.info/), and you should take a step back and describe the ultimate problem you are trying to solve, that you believe solving this sub-problem would get you closer to. – Igor Tandetnik Jun 10 '23 at 20:30
  • 1
    In general C++ should not work this way. When you have a polymorphic container you will need to store an (abstract) baseclass and access functionality through virtual methods. No typecasting should be needed. Or if you want a heterogenous collection of unrelated (but at compile time known types) objects you migh use a collection of std::variant. Also try not to use "new/delete" in C++ there are other options (containers/std::make_unique) that are better. – Pepijn Kramer Jun 10 '23 at 20:33
  • @IgorTandetnik I added an example of how I imagine it working. It's more or less how I did it for my C# project, but I am now rebuilding this project in C++ and can't achieve it. – Ryan Pergent Jun 10 '23 at 20:38
  • 1
    All in all I think you are still stuck in a "C#" way of thinking. C++ is a totally different language with different rules. So you might first need to go back to the basics and learn C++ from scratch. Just to give you an idea read on [scopes in C++](https://en.cppreference.com/w/cpp/language/scope). The {} actually manage lifetimes of objects (there is no garbage collector) – Pepijn Kramer Jun 10 '23 at 20:38
  • Good sources to learn cpp from are : A [recent C++ book](https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list) or have a go at https://www.learncpp.com/ (that's pretty decent, and pretty up-to-date). For C++ reference material use : [cppreference](https://en.cppreference.com/w/). And after you learned the C++ basics from those sources, look at the [C++ coreguidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) regularely to keep up-to-date with the latest guidelines. – Pepijn Kramer Jun 10 '23 at 20:39
  • @PepijnKramer I do agree with being stuck in C# way of thinking. I was hoping that someone here could help me figure out what would be the C++ approach to it. Telling me to "be better at C++", is not very helpful :( – Ryan Pergent Jun 10 '23 at 20:45
  • But why do you want this? What's the ultimate goal of the exercise? What will you do with the answer thus obtained? I mean, if you have a function that receives a `Labrador*` as a parameter, then it already knows the object is derived from `Dog`, and checking would be pointless. So what does that hypothetical function receive instead, and what would it do differently if the result of the check is `true` vs `false`? – Igor Tandetnik Jun 10 '23 at 20:48
  • 1
    No I am not telling you to do better at C++. I want to tell you C++ works with (totally) different rules as C# , and that sometimes they might seem similar but that doesn't mean they work the same. To solve your problems you really need to learn C++ as its independent language. – Pepijn Kramer Jun 10 '23 at 20:49
  • There is no way to do what you want in C++. As you already stated, the closest solutions to this in C++ require you to know the types in advance. If you can reduce your problem in scope to only deal with a fixed number of types, even if that number is large, you'll get better help. And if you can explain what you're trying to do (since this does sound like an XY problem), you can probably get a better C++ish solution for that. – Stephen Newell Jun 10 '23 at 20:52
  • @IgorTandetnik, the goal is not to know if `Labrador` is derived from `Dog`. The goal is to know if `Dog` is one of the `validTypes`. I have several contexts with different validTypes, and I need to know if an object is from one of the valid classes. I don't know what to tell more without explaining the totality of my project. – Ryan Pergent Jun 10 '23 at 20:53
  • Why would you need to check it? How would you use this piece of information? My guess: if the object passes the check, you would then pass it to client code that provided you with the list of types. If my guess is correct, then the right way to do what you want is to not do any checks in your code, pass it to client code as is, and let client code reject it if the type is not right. Client code is equipped to do so because it knows the right types *statically*. – n. m. could be an AI Jun 10 '23 at 20:53
  • 2
    I think you are trying to do reflection. C++ doesn't at this time support reflection. There's no way to represent "a list of types known only at runtime"; types have to be known statically, at compile time. (Well, to be precise, there's kinda sorta is - `std::type_info` and `std::type_index` - but that will only let you check "is same type", not "is derived from"). – Igor Tandetnik Jun 10 '23 at 21:00
  • @n.m. I have an `Action` class that has multiple classes inheriting from it. `Action` has a `canExecute` method that takes a target as a parameter and returns true if the target is from a certain class. Each class inheriting from Action would have their own `validTypes` list, that `canExecute` would check. I have more cases in my code where being able to check against a list of "validTypes" would be useful. – Ryan Pergent Jun 10 '23 at 21:02
  • Is there a base type for what gets passed to `canExecute`? – Stephen Newell Jun 10 '23 at 21:06
  • If `Action` has `execute`, there is no need in `canExecute`. Pass it to `execute` and let it be checked there and rejected if needed. – n. m. could be an AI Jun 10 '23 at 21:13
  • @StephenNewell, `canExecute` receives an `Interactible* target`. All validTypes will inherit from Interactible. – Ryan Pergent Jun 10 '23 at 21:19
  • Alternatively, if you insist on having `canExecute`, make it a virtual function that subclasses can override (instead of providing a list of valid types). – n. m. could be an AI Jun 10 '23 at 21:31
  • Types cannot be stored as data. But function pointers can. You can have a vector of functions `bool (*)(Action*)` where the function does a `dynamic_cast` to a specific type. – Raymond Chen Jun 11 '23 at 00:14

2 Answers2

1

The short answer is no.

The longer answer is that C++ is very rigid on one point: all types are static. So while we can do things like creating lists of types, passing types as parameters, and so on, all of that is done with templates, and it all happens entirely at compile time.

If you really and truly need this, you're pretty much stuck with using values rather than types. That will probably involve each Action storing a list of values that are unique to particular types, and doing a search at runtime to figure out whether a particular value is in its list at any given time.

At least offhand, this strikes me as creating a situation that's (at best) incredibly confusing for the user. If I, as a user, try to do something one day, and it works, but try to do the same thing few minutes later or the next day and it doesn't work, I usually think of that as a bug. But at least as far as I can tell, that seems to be precisely what you're trying to do.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • Thank you for the clarification. Knowing that it is impossible, I can start looking into other solutions. I don't understand the last part about the confusion. Why would something not work a few minutes later? – Ryan Pergent Jun 11 '23 at 07:52
  • @RyanPergent: You said this is driven by lists of types that have to be determined at run time. That indicates that you must be changing those lists at run time. So, you have something that works, then when a list changes, your behavior changes--either something works that didn't before, or something doesn't work that did before. Either seems likely to lead to confusion. – Jerry Coffin Jun 11 '23 at 15:46
  • Oh okay. No, no worries that won't be the case. It is decided at runtime but won't change arbitrarily. The user will know exactly what to expect. I just want to use a list of types because it's less cumbersome to work with than doing a different virtual method for each of my class that inherit from Action. – Ryan Pergent Jun 11 '23 at 17:48
  • @RyanPergent: That description makes it sound like you may want to look at the visitor pattern. – Jerry Coffin Jun 11 '23 at 19:55
1

As far as I know, there are three situations where the type of an object is looked up at runtime: with dynamic_cast, binding exception objects to handlers and looking up which virtual function to call.

Here's an implementation of using exception handlers to store type information at runtime: https://godbolt.org/z/Mf71bbr7P

#include <type_traits>
#include <typeinfo>

namespace detail {
    template<typename T>
    struct CType;
}

struct Type {
    const std::type_info& type_info;

    template<typename T>
    friend struct detail::CType;

    template<typename T>
    friend bool IsOrInheritsFrom(const T&, const Type& type) noexcept {
        return type.can_catch_thrown_pointer([]{ throw static_cast<T*>(nullptr); });
    }

    Type(const Type&) = delete;
    Type& operator=(const Type&) = delete;
private:
    constexpr Type(const std::type_info& ti) noexcept : type_info(ti) {}
    ~Type() = default;
    virtual bool can_catch_thrown_pointer(void thrower()) const noexcept = 0;
};

namespace detail {
    template<typename T>
    struct CType : Type {
        static const CType instance;
    private:
        constexpr CType() noexcept : Type(typeid(T)) {}
        ~CType() = default;
        bool can_catch_thrown_pointer(void thrower()) const noexcept override {
            try {
                thrower();
            } catch (T*) {
                return true;
            } catch (...) {}
            return false;
        }
    };

    template<typename T>
    constexpr CType<T> CType<T>::instance{}; 
}

template<typename T>
constexpr const Type& TypeOf = detail::CType<typename std::remove_cv<typename std::remove_reference<T>::type>::type>::instance;


#include <vector>
#include <iostream>

struct Dog {};
struct Cat {};
struct Hamster {};
struct Labrador : Dog {};

int main() {
    std::vector<const Type*> validTypes;
    validTypes.push_back(&TypeOf<Dog>);
    validTypes.push_back(&TypeOf<Cat>);
    validTypes.push_back(&TypeOf<Hamster>);

    Labrador* somePet = new Labrador();

    for (const Type* t : validTypes) {
        if (IsOrInheritsFrom(*somePet, *t)) {
            std::cout << "Inherits from " << t->type_info.name() << '\n';
        } else {
            std::cout << "Does not inherit from " << t->type_info.name() << '\n';
        }
    }
}

Which throws (Labrador*) nullptr and each type in the vector sees if it can catch it. And it can be caught as a Dog*.


You can do something similar with dynamic_cast. The only way I can think of doing it is having a common base class between all types you want to be able to check, then each runtime type can try to dynamic_cast: https://godbolt.org/z/s9oearhGP

#include <type_traits>
#include <typeinfo>
#include <memory>

struct RuntimeTypeCheckable {
protected:
    RuntimeTypeCheckable() = default;
    RuntimeTypeCheckable(const RuntimeTypeCheckable&) = default;
    RuntimeTypeCheckable& operator=(const RuntimeTypeCheckable&) = default;
    virtual ~RuntimeTypeCheckable() = default;
};

namespace detail {
    template<typename T>
    struct CType;
}

struct Type {
    const std::type_info& type_info;

    template<typename T>
    friend struct detail::CType;

    template<typename T>
    friend bool IsOrInheritsFrom(const T& object, const Type& type) noexcept {
        return type.can_dynamic_cast_pointer(std::addressof(object));
    }

    Type(const Type&) = delete;
    Type& operator=(const Type&) = delete;
private:
    constexpr Type(const std::type_info& ti) noexcept : type_info(ti) {}
    ~Type() = default;
    virtual bool can_dynamic_cast_pointer(const RuntimeTypeCheckable* ptr) const noexcept = 0;
};

namespace detail {
    template<typename T>
    struct CType : Type {
        static const CType instance;
    private:
        constexpr CType() noexcept : Type(typeid(T)) {}
        ~CType() = default;
        bool can_dynamic_cast_pointer(const RuntimeTypeCheckable* ptr) const noexcept override {
            return dynamic_cast<const T*>(ptr) != nullptr;
        }
    };

    template<typename T>
    constexpr CType<T> CType<T>::instance{}; 
}

template<typename T>
constexpr const Type& TypeOf = detail::CType<typename std::remove_cv<typename std::remove_reference<T>::type>::type>::instance;


#include <vector>
#include <iostream>

struct Dog : virtual RuntimeTypeCheckable {};
struct Cat : virtual RuntimeTypeCheckable {};
struct Hamster : virtual RuntimeTypeCheckable {};
struct Labrador : Dog {};

int main() {
    std::vector<const Type*> validTypes;
    validTypes.push_back(&TypeOf<Dog>);
    validTypes.push_back(&TypeOf<Cat>);
    validTypes.push_back(&TypeOf<Hamster>);

    Labrador* somePet = new Labrador();

    for (const Type* t : validTypes) {
        if (IsOrInheritsFrom(*somePet, *t)) {
            std::cout << "Inherits from " << t->type_info.name() << '\n';
        } else {
            std::cout << "Does not inherit from " << t->type_info.name() << '\n';
        }
    }
}

Though this requires the types to have RuntimeTypeCheckable as a base class.


As for virtual functions, you would need something like this: https://godbolt.org/z/jq6b8E7bx

#include <typeinfo>

struct RuntimeTypeCheckable {
public:
    virtual bool is_or_inherits_from(const std::type_info& type_info) const noexcept {
        return type_info == typeid(RuntimeTypeCheckable);
    }

    friend bool IsOrInheritsFrom(const RuntimeTypeCheckable& object, const std::type_info& type) noexcept {
        return object.is_or_inherits_from(type);
    }
protected:
    RuntimeTypeCheckable() = default;
    RuntimeTypeCheckable(const RuntimeTypeCheckable&) = default;
    RuntimeTypeCheckable& operator=(const RuntimeTypeCheckable&) = default;
    virtual ~RuntimeTypeCheckable() = default;
};


#include <typeindex>
#include <vector>
#include <iostream>

struct Dog : virtual RuntimeTypeCheckable {
    bool is_or_inherits_from(const std::type_info& type_info) const noexcept override {
        return type_info == typeid(Dog) || RuntimeTypeCheckable::is_or_inherits_from(type_info);
    }
};
struct Cat : virtual RuntimeTypeCheckable {
    bool is_or_inherits_from(const std::type_info& type_info) const noexcept override {
        return type_info == typeid(Dog) || RuntimeTypeCheckable::is_or_inherits_from(type_info);
    }
};
struct Hamster : virtual RuntimeTypeCheckable {
    bool is_or_inherits_from(const std::type_info& type_info) const noexcept override {
        return type_info == typeid(Dog) || RuntimeTypeCheckable::is_or_inherits_from(type_info);
    }
};
struct Labrador : Dog {
    bool is_or_inherits_from(const std::type_info& type_info) const noexcept override {
        return type_info == typeid(Labrador) || Dog::is_or_inherits_from(type_info);
    }
};

int main() {
    std::vector<const std::type_info*> validTypes;
    validTypes.push_back(&typeid(Dog));
    validTypes.push_back(&typeid(Cat));
    validTypes.push_back(&typeid(Hamster));

    Labrador* somePet = new Labrador();

    for (const std::type_info* t : validTypes) {
        if (IsOrInheritsFrom(*somePet, *t)) {
            std::cout << "Inherits from " << t->name() << '\n';
        } else {
            std::cout << "Does not inherit from " << t->name() << '\n';
        }
    }
}

Which requires the most modifications to the types being used, but is the most readable. The boilerplate can be reduced with CRTP.

Artyer
  • 31,034
  • 3
  • 47
  • 75