Colin Weltin-Wu
Colin Weltin-Wu

Reputation: 377

Arrays of template class objects

Problem

I would like an array of pointers to instances of a template class. My problem would be solved if C++ allowed templated virtual methods in a base class, with a templated derived class.

Therefore, how would one implement templated virtual methods?

Below I have a solution which seems to work, but I'm interested in comments about my implementation.

Constraints

The template parameter is infinitely variable, e.g., I cannot enumerate every specialization of this template class. The template class T can be any POD, array of POD, or struct of POD.

The complete set of T is known at compile time. Basically, I have a file which defines all the different T used to instantiate the objects, and use Xmacros (https://en.wikipedia.org/wiki/X_Macro) to create the array of objects.

I know this isn't a great idea. Let's gloss over that for the time being. This ends up being more a curiosity.

Possible Solutions

These are the things I've looked into.

Create base and derived classes

class Base {
  virtual void SomeMethod() = 0;
}

template <class T>
class Derived : Base {
  void SomeMethod() {...}
}

The problem with this is I cannot declare all the virtual methods in Base that I want to overload, as virtual methods cannot be templated. Otherwise, it would be a perfect solution.

std::any/std::variant

I am using C++17, so I could define the virtual base methods taking std::any. But it cannot hold arrays, which precludes its use here.

CRTP

It seems this would not help me create an array of these different objects. I would need to do something like

template <typename D, typename T>
class Base
{
    ...
};

template <typename T>
class Derived : public Base<Derived, T>
{
    ...
};

So I still end up with trying to create an array of Derived<T> objects.

Visitor Pattern

Again it looks like I would need to enumerate every possible type the Visitable class needs to service, which, while not impossible (again, I have a file which defines all the different T that will be used) seems like more Xmacros, which is just making the problem more complicated.

My Solution

This is what I came up with. It will run in https://www.onlinegdb.com/online_c++_compiler

#include <iostream>
#include <array>
#include <typeinfo>

// Base class which declares "overloaded" methods without implementation
class Base {
 public:
  template <class T>
  void Set(T inval);
  template <class T>
  void Get(T* retval);
  virtual void Print() = 0;
};

// Template class which implements the overloaded methods
template <class T>
class Derived : public Base {
 public:
  void Set(T inval) {
    storage = inval;
  }
  void Get(T* retval) {
    *retval = storage;
  }
  void Print() {
    std::cout << "This variable is type " << typeid(T).name() <<
      ", value: " << storage << std::endl;
  }
 private:
  T storage = {};
};

// Manually pointing base overloads to template methods
template <class T> void Base::Set(T inval) {
  static_cast<Derived<T>*>(this)->Set(inval);
}
template <class T> void Base::Get(T* retval) {
  std::cout << "CALLED THROUGH BASE!" << std::endl;
  static_cast<Derived<T>*>(this)->Get(retval);
}

int main()
{
  // Two new objects
  Derived<int>* ptr_int = new Derived<int>();
  Derived<double>* ptr_dbl = new Derived<double>();
  
  // Base pointer array
  std::array<Base*, 2> ptr_arr;
  ptr_arr[0] = ptr_int;
  ptr_arr[1] = ptr_dbl;

  // Load values into objects through calls to Base methods
  ptr_arr[0]->Set(3);
  ptr_arr[1]->Set(3.14);

  // Call true virtual Print() method
  for (auto& ptr : ptr_arr) ptr->Print();

  // Read out the values
  int var_int;
  double var_dbl;
  std::cout << "First calling Get() method through true pointer." << std::endl;
  ptr_int->Get(&var_int);
  ptr_dbl->Get(&var_dbl);
  std::cout << "Direct values: " << var_int << ", " << var_dbl << std::endl;
  std::cout << "Now calling Get() method through base pointer." << std::endl;
  ptr_arr[0]->Get(&var_int);
  ptr_arr[1]->Get(&var_dbl);
  std::cout << "Base values: " << var_int << ", " << var_dbl << std::endl;

  return 0;
}

When this is run, it shows that calling the methods on Base correctly point to the Derived implementations.

This variable is type i, value: 3                                                                                                    
This variable is type d, value: 3.14                                                                                                 
First calling Get() method through true pointer.                                                                                     
Direct values: 3, 3.14                                                                                                               
Now calling Get() method through base pointer.                                                                                       
CALLED THROUGH BASE!                                                                                                                 
CALLED THROUGH BASE!                                                                                                                 
Base values: 3, 3.14  

Essentially I am manually creating the virtual method pointers. But, since I am explicitly doing so, I am allowed to use template methods in Base which point to the methods in Derived. It is more prone to error, as for example for each template method I need to type the method name twice, i.e., I could mess up:

template <class T> void Base::BLAH_SOMETHING(T inval) {
  static_cast<Derived<T>*>(this)->WHOOPS_WRONG_CALL(inval);
}

So after all this, is this a terrible idea? To me it seems to achieve my objective of circumventing the limitation of templated virtual methods. Is there something really wrong with this? I understand there could be ways to structure the code that make all this unnecessary, I am just focusing on this specific construction.

Upvotes: 1

Views: 1202

Answers (2)

n. m. could be an AI
n. m. could be an AI

Reputation: 119877

The normal run-off-the-mill method of dealing with subclasses that contain unknown types is to move the entire thing to a virtual function. Thus, instead of

superclass->get_value(&variable_of_unknown_type);
print(variable_of_unknown_type);

you write

superclass->print_value();

Now you don't need to know about any of the types a subclass might contain.

This is not always appropriate though, because there could be lots of operations. Making every operation a virtual function is troublesome if you are adding new operations all the time. On the other hand, the set of possible subclasses is often limited. In this case your best bet is the Visitor. Visitor rotates the inheritance hierarchy 90°, so to speak. Instead of fixing the set of operations and adding new subclasses freely, you fix the set of subclasses and add new operations freely. So instead of

superclass->print_value();

you write

class PrinterVisitor : public MyVisitor
{
   virtual void processSubclass1(Subclass1* s) { print(s->double_value); }
   virtual void processSubclass2(Subclass2* s) { print(s->int_value); }
};

superclass->accept(PrinterVisitor());

Now accept is the only virtual function in your base class. Note there are no casts that could possibly fail anywhere in the code.

Upvotes: 0

krisz
krisz

Reputation: 2695

It is more prone to error, as for example for each template method I need to type the method name twice

Oh, that's the least of your concerns. Imagine if you downcast to the wrong type.

At least save yourself a headache and use dynamic_cast:

class Base {
  public:
    virtual ~Base() = default;

    template <class T>
    void Set(T inval) {
        dynamic_cast<Derived<T>&>(*this).Set(inval);
    }

    template <class T>
    T Get() {
        return dynamic_cast<Derived<T>&>(*this).Get();
    }
};


template <class T>
class Derived : public Base {
  public:
    void Set(T inval) {
      storage = inval;
    }

    T Get() {
      return storage;
    }

  private:
    T storage{};
};

Other than that, I agree with the comments, this is probably not the right approach to your problem.

Upvotes: 1

Related Questions