Can C++ templates provide the common parent class of N given classes?

Viewed 252

I am looking for a c++ template which finds the common parent of a set of given classes.

For example

class Animal { ... };
class Mammal : public Animal { ... };
class Fish   : public Animal { ... };
class Cat    : public Mammal { ... };
class Dog    : public Mammal { ... };

std::unique_ptr<common_ancestor_of<Cat,Dog>::type> a = new Cat();
std::unique_ptr<common_ancestor_of<Cat,Dog>::type> b = new Dog();
std::unique_ptr<common_ancestor_of<Cat,Dog>::type> c = new Fish(); // compile error
std::unique_ptr<common_ancestor_of<Cat,Dog,Fish>::type> d = new Fish();

a and b are both std::unique_ptr<Mammal>, c is std::unique_ptr<Animal>.

How is this possible with modern C++?

2 Answers

Afaik no, there is no way in C++ to get the base class of a given class. There is introspection (part of reflection) in the works, but I wouldn't hold my breath for it to go into the standard any time soon.

The only way would be to make the classes cooperate. E.g. Have each class a member alias using Base = Animal. And then cook a trait that finds the common base between them. That would be a lot of work. You need to take into account multiple base classes and chains of inheritance. It's not trivial. You need to analyze your problem and see if all this complicated work is worth it or if there is another simpler way for what you are trying to achieve (which btw you din't mention). You may have an XY problem on your hand.

No, common_ancestor_of would require reflection to get the base class of any of the given classes.

If you have a concrete set of classes, you could use std::variant of all the possible classes.

If not, you could use a base class trait (Either a struct base_class that you specialise for all of your types or a member type like using super = ... or using base = ...) and manually find the common ancestor:

template<typename T>
struct type_identity {
    using type = T;
};

template<typename... Types>
struct common_ancestor_of;

template<typename T>
struct common_ancestor_of<T> {
    using type = T;
};

template<typename T>
struct common_ancestor_of<T, T> {
    using type = T;
};

template<typename T, typename U, typename... Rest>
struct common_ancestor_of<T, U, Rest...> : common_ancestor_of<typename common_ancestor_of<T, U>::type, Rest...> {};

template<typename T, typename U>
struct common_ancestor_of<T, U> {
private:
    // Base == Derived, so is in it's inheritance chain
    template<typename Base, typename Derived, typename std::enable_if<std::is_same<Base, Derived>::value, int>::type = 0>
    static constexpr bool in_inheritance_chain(int) {
        return true;
    }

    // Base != Derived, but Derived has a member type `super`, so recursively check `super`
    template<typename Base, typename Derived, typename std::enable_if<!std::is_same<Base, Derived>::value && (noexcept(type_identity<typename Derived::super>{}), true), int>::type = 0>
    static constexpr bool in_inheritance_chain(int) {
        return in_inheritance_chain<Base, typename Derived::super>(0);
    }

    // Base != Derived and Derived doesn't have a member type `super`, so it isn't in the inheritance chain
    template<typename Base, typename Derived>
    static constexpr bool in_inheritance_chain(long) {
        return false;
    }

    // T1 is in the inheritance chain for U1, so it is the common ancestor
    template<typename T1, typename U1, typename std::enable_if<in_inheritance_chain<T1, U1>(0), int>::type = 0>
    static type_identity<T1> find_common_ancestor(int);
    // T1 is not in the inheritance chain, so check T1::super
    template<typename T1, typename U1>
    static decltype(find_common_ancestor<typename T1::super, U1>(0)) find_common_ancestor(long) {}
public:
    using type = typename decltype(find_common_ancestor<T, U>(0))::type;
};

template<typename... Types>
using common_ancestor_of_t = typename common_ancestor_of<Types...>::type;


class Animal { };
class Mammal : public Animal { public: using super = Animal; };
class Fish   : public Animal { public: using super = Animal; };
class Cat    : public Mammal { public: using super = Mammal; };
class Dog    : public Mammal { public: using super = Mammal; };

static_assert(std::is_same<typename Cat::super::super, Animal>::value);

static_assert(std::is_same<common_ancestor_of_t<Cat, Dog>, Mammal>::value);
static_assert(std::is_same<common_ancestor_of_t<Cat, Fish>, Animal>::value);
static_assert(std::is_same<common_ancestor_of_t<Fish, Cat>, Animal>::value);
static_assert(std::is_same<common_ancestor_of_t<Cat, Dog, Fish>, Animal>::value);

This gets the most specialised common ancestor, but consider using the common ancestor you already have: std::unique_ptr<Animal>. If you specifically write std::unique_ptr<common_ancestor_of_t<Cat, Dog>> in your code, it is just as easy to write std::unique_ptr<Mammal>. If it's behind some templated code, std::unique_ptr<Animal> should work just as well.

Related