Search code examples
c++ooptemplatesmultiple-inheritance

How to do actions on members of child class templates from an abstact base class?


I know this question gets asked a lot, but I have a specific use case, so I don't think it's a duplicate!

I have an abstract base class:

template<int N>
class Child;

class Base
{

public:
     // Factory-like generation of children as Base
     static Ptr<Base> New(int baseN)
     {
         if (baseN == 2) return new Child<2>(); 
         else if (baseN == 3) return new Child<3>()
     }

     // Update
     virtual void update() = 0;
};

And I'm writing some children of Base as class templates (on an int):

template<int N>
class Child
:
    public Base
{
     // Member, N is not the size of matrix, more like the size of a component in matrix
     Matrix<N> m_member;

public:
     // Implement update
     virtual void update();

     // Should call the passed callable on m_member
     virtual void execute(std::function<void(Matrix<N>&)>&);
};

// Force compilation of Child<N>  for some values of N (of interest, including 3) here

// Then,
int baseN = 3;
Ptr<Base> obj = Base::New(baseN); // will get me a Child<3> as a Base object


auto callable = [](Matrix<3>) ->void {};

// Can I access Child<3>::m_member ??
// Can't cast to Child<baseN> (baseN is not constexpr) and don't want to
// But want to do something like:
obj->execute(callable);
// Which forwards 'callable' to the method from concrete type, probably using a cast?

In short, I need to have some sort of access to m_member from the declared Base object. Preferably, a way to call Child<N>::execute from Base without making Base a template on N too.

Things I've tried/thought-of include:

  1. 'Type erasure' of Matrix<N> by hiding them behind an interface, but because Matrix<N>'s interface strongly depends on N, doing that renders the classes useless (think: Vector<N>& Matrix<N>::diag() for example)
  2. Can Base::New do anything to record what concrete type it creates? I doubt that because types are not objects.

EDIT: (Btw this is C++11)

So, I accidentally figured out a way to do this; but I don't quite understand why the following works (Not well versed into assembly yet):

  • I'm using a Database for objects (unordered_map<string, object*> where object is a class that every registered object has to inherit from).
  • When a Child is created, we register it to a database with a name of Child<N>.
  • Then, at application-level code, there is a findChild<int N> template which employs compile-time recursion to find which concrete class was the Base pointer created from (At runTime, by dynamicCasting and testing). When It finds it, it can cast it to void* through a static method (findChild<N>::castToConcrete)
  • What's interesting is that we can somehow use findChild<0> to access the findChild<N> in question if Child<N> is polymorphic. This forces us to have at most one object of Child (for all possible Ns) and I certainly can live with that.

You can see and inspect a minimal code example here: https://onlinegdb.com/CiGR1Fq5z

What I'm so confused about is that Child<0> and other Child<N> are completely different types; So how can we access one's members from a pointer to another type? I'm most likely relying on UB and even fear there is a stack smacking of some sort!

For reference, I'm including the code here in case the link dies.

#include <unordered_map>
#include <vector>
#include <functional>
#include <iostream>

using namespace std;

#ifndef MAX_N_VALUE
    #define MAX_N_VALUE 10
#endif // !MAX_N_VALUE

// ------------------ Lib code

// A dummy number class for testing only
template <int N> struct Number { constexpr static int value = N; };

// Objects to register to the database
struct object
{
    // Members
    string name;

    // construction/Destruction
    object(const string& name) : name(name) {}
    virtual ~object(){};
};


// Database of objects
struct DB
: public unordered_map<string, object*>
{
    // See if we can the object of name "name" and type "T" in the DB
    template <class T>
    bool found(const string& name) const
    {
        unordered_map<string,object*>::const_iterator iter = find(name);
        if (iter != end())
        {
            const T* ptr = dynamic_cast<const T*>(iter->second);
            if (ptr) return true;
            cout << name << " found but it's of another type." << endl;
            return false;
        }
        cout << name << " not found." << endl;
        return false;
    }

    // Return a const ref to the object of name "name" and type "T" in the DB
    // if found. Else, fails
    template <class T>
    const T& getObjectRef(const string& name) const
    {
        unordered_map<string,object*>::const_iterator iter = find(name);
        if (iter != end())
        {
            const T* ptr = dynamic_cast<const T*>(iter->second);
            if (ptr) return *ptr;
            cout << name << " found but it's of another type." << endl;
            abort();
        }
        cout << name << " not found." << endl;
        abort();
    }
};


// Forward declare children templates
template<int N>
class Child;

// The interface class
struct Base
{
    // Construction/Destruction
protected:
    static unsigned counter;
    Base(){}
public:
    virtual ~Base() {}

    // Factory-like generation of children as Base
    // THIS New method needs to know how to construct Child<N>
    // so defining it after Child<N>
    static Base* New(int baseN, DB& db);

    // Update
    virtual void update() = 0;
    
    // Call a callable on a child, the callable interface
    // however is independent on N
    virtual void execute(std::function<void(Base&)>& callable)
    {
        callable(*this);
    }
};

unsigned Base::counter = 0;

// The concrete types, which we register to the DB
template<int N>
struct Child
:
    public Base, public object
{
    // members
    vector<Number<N>> member;

    // Construction/Destruction 
    Child() : Base(), object(string("Child") + to_string(N) + ">"), member(N, Number<N>()) {}
    virtual ~Child() {}

    // Test member method (Has to be virtual)
    virtual vector<Number<N>> test() const
    {
        cout << "Calling Child<" << N << ">::test()" << endl;
        return vector<Number<N>>(N, Number<N>());
    }

    // Implement update
    virtual void update()
    {
        cout << "Calling Child<" << N << ">::update()" << endl;
    };
};

// New Base, This can be much more sophisticated
// if static members are leveraged to register constructors
// and invoke them on demand.
Base* Base::New(int baseN, DB& db)
{
    if (baseN == 2)
    {
        Child<2>* c = new Child<2>();
        db.insert({string("Child<")+std::to_string(2)+">", c});
        return c;
    }
    if (baseN == 3)
    {
        Child<3>* c = new Child<3>();
        db.insert({string("Child<")+std::to_string(3)+">", c});
        return c;
    }
    return nullptr;
}

// Finder template for registered children
template<int N>
struct findChild
{
    // Concrete Type we're matching against
    using type = Child<N>;

    // Stop the recursion?
    static bool stop;

    // Compile-time recursion until the correct Child is caught
    // Recursion goes UP in N values
    static void* castToConcrete(const DB& db, Base* system)
    {
        if (N > MAX_N_VALUE) stop = true;
        if (stop) return nullptr;
        if (db.found<type>(string("Child<")+to_string(N)+">"))
        {
            type* ptr = dynamic_cast<type*>(system);
            return static_cast<void*>(ptr);
        }
        // NOTE: This should jump to the next "compiled" child, not just N+1, but meh;
        return findChild<N+1>::castToConcrete(db, system);
    }
};

// Activate recursive behaviour for arbitraty N
template<int N>
bool findChild<N>::stop = false;

// Explicit specialization to stop the Compile-time recursion at a decent child
template<>
struct findChild<MAX_N_VALUE+1>
{
    using type = Child<MAX_N_VALUE+1>;
    static bool stop;
    static void* castToConcrete(const DB& t, const Base* system)
    {
        return nullptr;
    }
};

// Disactivate recursive behaviour for N = 11
bool findChild<MAX_N_VALUE+1>::stop = true;


// ------------------ App code

int main()
{
    // Create objects database
    DB db;

    // --- Part 1: Application writers can't write generic-enough code

    // Select (from compiled children) a new Base object with N = 2
    // and register it to the DB
    Base* b = Base::New(2, db);
    b->update();

    cout << "Access children by explicit dynamic_cast to Child<N>:" << endl;

    // Get to the object through the objects DB.
    // Child destructor should remove the object from DB too, nut meh again
    const auto& oo = db.getObjectRef<Child<2>>("Child<2>");
    cout << oo.test().size() << endl;

    // --- Part 2: Application writers can write generic code if the compile
    // Child<N> for their N

    cout << "If Child<N> is polymorphic, we can access the correct child from findChild<0>:" << endl;

    // Create a lambda that knows about db, which Base applies on itself
    function<void(Base&)> lambda = [&db](Base& base) -> void {
        // Cast and ignore the result
        void* ptr = findChild<0>::castToConcrete(db, &base);

        // Cast back to Child<0>
        findChild<0>::type* c = static_cast<findChild<0>::type*>(ptr);

        // Now access original Child<N> methods and members from Child<0>
        cout << "Method:\n" << c->test().size() << endl;
        cout << "Member:\n" << c->member.size() << endl;
    };

    b->execute(lambda);

    return 0;
}

I compiled with GCC 9 with the following options:

-m64 -Wall -Wextra -Wno-unused-parameter -Wold-style-cast -Wnon-virtual-dtor -O0 -fdefault-inline -ftemplate-depth-200

Solution

  • It seems you want inheritance to group not so related classes...

    std::variant (C++17) might be more appropriate:

    template<int N>
    class Child
    {
         // Member, N is not the size of matrix, more like the size of a component in matrix
         Matrix<N> m_member;
    
    public:
         void update();
    
         void execute(std::function<void(Matrix<N>&)> f) { f(m_member); }
    };
    
    using Base = std::variant<Child<2>, Child<3>>;
    

    and then:

    void foo(Base& obj)
    {
        struct Visitor {
            template <std::size_t N>
            void operator()(Child<N>& c) const
            {
                auto callable = [](Matrix<N>) -> void {/*..*/};
                c.execute(callable);
            }
        } visitor;
        std::visit(visitor, obj);
    }
    

    To answer to your Edit, whereas your callable take a Base, you might chain the dynamic_cast as follow:

    template <int N>
    void foo_base(Base& b)
    {
        if (auto* child = dynamic_cast<Child<N>*>(&b)) {
            // Job with Child<N>
            std::cout << "Method:" << child->test().size() << std::endl;
            std::cout << "Member:" << child->member.size() << std::endl;
        }
    }
    
    template <int... Ns>
    void foo_dispatch(std::integer_sequence<int, Ns...>, Base& base)
    {
        //(foo_base<Ns>(base), ...); // C++17
        const int dummy[] = {0, (foo_base<Ns>(base), 0)...};
        static_cast<void>(dummy); // Avoid warning about unused variable
    }
    

    With a call similar to:

    function<void(Base&)> lambda = [](Base& base) {
        //foo_dispatch(std::integer_sequence<int, 2, 3>(), base);
        foo_dispatch(std::make_integer_sequence<int, MAX_N_VALUE>(), base);
    };
    

    Demo

    (std::integer_sequence is C++14, but can be implemented in C++11)