1

Suppose I have two abstract classes called Color and Animal

And I can create classes Green/Red/Blue derived from Color and classes Dog/Cat/Pig derived from Animal at runtime using factory pattern.

Then, I have a template class Hybrid:

template<class A, class B>
class Hybrid
: public A, public B
{}

What I want to do is to instantiate the template class Hybrid with the derived classes such as Green and Dog being created at runtime.

Is this possible? Or is there any other way to achieve similar functionability?

1
  • 1
    Think of templates more like a smart textual pre-processor/code-generation tool. The fundamental output is code, unadorned with any template-related metadata. As far as the program is concerned at runtime, templates don't exist.
    – Alexander
    Commented Sep 2, 2022 at 17:10

3 Answers 3

6

The short answer is that C++ templates do not exist anywhere in any compiled code and are not available at runtime.

Template instantiation is a code generation feature of C++, and therefore strictly limited to compile-time:

A class template by itself is not a type, or an object, or any other entity. No code is generated from a source file that contains only template definitions. In order for any code to appear, a template must be instantiated: the template arguments must be provided so that the compiler can generate an actual class (or function, from a function template).

https://en.cppreference.com/w/cpp/language/class_template

C++ is a statically typed language, and also does not include concepts from other languages such as reflection, furthermore, C++ compilers (unlike some other languages like C# where some metadata is preserved and aspects of the type system exist at runtime) typically do not have any reason to add any other compile-time metadata into the compiled binaries.

So as far as the C++ language itself is concerned, there is no support for the use of its type system at runtime to declare, define or alter types.

2

The only way I know to achieve what you asked for literally is to

  • provide all 9 possible combinations of template instantiations somewhere in your code, and use this to fill a 3x3 matrix of objects of each type combination

  • make sure each of your Animal and Color classes provide an index from 0 to 2 in their derived class, where, for example, index 0 is returned for a Green object, 1 for a Red object and index 2 for a Blue object.

Now you can use the two indexes to find the corresponding object in the matrix at run time.

But beware - templates in C++ are not covariant - so your matrix cannot be just of type Hybrid<Color,Animal>*[3][3] . Instead, you will have to provide some common interface type, lets call it IHybrid, derive Hybrid from A, B and IHybrid, and use a matrix of the kind IHybrid*[3][3].

Note, the fact this problem has a solution does not mean this is a good one. I would usually try to avoid such an approach, since it does not scale well with an increasing number of derived classes. Instead, I would make Hybrid simply a class which contains two objects / pointers of type Color and Animal as members (with no inheritance involved).

2

As the answer from Ben Cottrell explains, templates are a purely compile-time concept. They cannot be applied at runtime.

If you want to provide similar functionality dynamically at runtime, you will more or less have to invent your own object system.

A simple approach to address these issues is to replace inheritance with composition. Instead of having a class Hybrid that inherits behaviours from Cat, Dog, Pig, you might have a class Animal that contains CatBehaviours, DogBehaviours, or PigBehaviours. So instead of:

class Hybrid: public Cat, Dog, Pig { ... };

void dogOnly(Dog&);

Hybrid instance;
dogOnly(instance);

… we might do:

class Animal {
public:
  CatBehaviour* as_cat = nullptr;
  DogBehaviour* as_dog = nullptr;
  PigBehaviour* as_pig = nullptr;
};

void dogOnly(DogBehaviour&);

Animal instance;
instance.as_cat = &CAT_BEHAVIOUR;
instance.as_dog = &DOG_BEHAVIOUR;

if (auto as_dog = instance.as_dog) {
  dogOnly(*as_dog);
} else {
  throw "cannot interact with this animal as a dog";
}

Since everything is dynamic, we have no static guarantees whether some object will provide particular behaviours – we have to query for support at runtime.

The above design does have difficulties with certain object-oriented patterns, in particular if the DogBehaviour needs access to data of the animal. Then, it might be necessary that the Behaviour objects are like adapters or decorators and get a pointer back to the animal. Here's a more production-grade example:

class DogBehaviour;
class CatBehaviour;
class PigBehaviour;

class Animal {
public:
  Animal(std::string name) :m_name(name) {}

  std::string const& name() const { return m_name; }

  DogBehaviour const* as_dog() const { return m_as_dog.get(); }
  DogBehaviour      * as_dog()       { return m_as_dog.get(); }
  CatBehaviour const* as_cat() const { return m_as_cat.get(); }
  CatBehaviour      * as_cat()       { return m_as_cat.get(); }
  PigBehaviour const* as_pig() const { return m_as_pig.get(); }
  PigBehaviour      * as_pig()       { return m_as_pig.get(); }

  void support_dog_behaviours();
  void support_cat_behaviours();
  void support_pig_behaviours();

private:
  std::string m_name;
  std::unique_ptr<DogBehaviour> m_as_dog;
  std::unique_ptr<CatBehaviour> m_as_cat;
  std::unique_ptr<PigBehaviour> m_as_pig;
};

class DogBehaviour {
public:
  DogBehaviour(Animal* a) : animal(a) {}

  void say_hello() const {
    std::cout << "Woof, I am " << animal->name() << std::endl;
  }

private:
  Animal* animal;
};

class CatBehaviour { ... };
class PigBehaviour { ... };

void Animal::support_dog_behaviours() {
  m_as_dog = std::make_unique<DogBehaviour>(this);
}

void Animal::support_cat_behaviours() { ... }
void Animal::support_pig_behaviours() { ... }

Then we can use this:

Animal instance("Fred");
// enable desired behaviours
instance.support_dog_behaviours();
instance.support_cat_behaviours();

// use behaviours, if they exist
if (auto instance_as_dog = instance.as_dog()) {
  instance_as_dog->say_hello();
}

But this too might be undesirable because it hardcodes all available classes. We can avoid this by maintaining a vector of behaviours. Such a vector would require all behaviours to have the same type, which we can achieve via std::any or via a base class for behaviours.

// empty marker interface
class Behaviour {
public:
  virtual ~Behaviour() = default;
};

class Animal {
public:
  Animal(std::string name) :m_name(name) {}

  std::string const& name() const { return m_name; }

  // find the first matching behaviour
  template<class B>
  B const* as() const {
    for (auto const& behaviour : m_behaviours) {
      if (auto b = dynamic_cast<B const*>(behaviour.get())) return b;
    }
    return nullptr;
  }

  template<class B>
  B* as() {
    for (auto& behaviour : m_behaviours) {
      if (auto b = dynamic_cast<B*>(behaviour.get())) return b;
    }
    return nullptr;
  }

  template<class B, class... Args>
  void support(Args&&... args) {
    auto b = std::make_unique<B>(this, std::forward<Args>(args)...);
    m_behaviours.push_back(std::move(b));
  }

private:
  std::string m_name;
  std::vector<std::unique_ptr<Behaviour>> m_behaviours;
};

class DogBehaviour: public Behaviour {
public:
  DogBehaviour(Animal* a) : animal(a) {}

  void say_hello() const {
    std::cout << "Woof, I am " << animal->name() << std::endl;
  }

private:
  Animal* animal;
};

struct CatBehaviour: public Behaviour { ... };
struct PigBehaviour: public Behaviour { ... };

Then:

Animal instance("Fred");
// enable desired behaviours
instance.support<DogBehaviour>();
instance.support<CatBehaviour>("meowmeow");

// use behaviours, if they exist
if (auto instance_as_dog = instance.as<DogBehaviour>()) {
  instance_as_dog->say_hello();
}
1
  • 1
    I think your answer would way better address the question when it would include a scetch on how to "inherit" a combination of animal and color at run time.
    – Doc Brown
    Commented Sep 3, 2022 at 2:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.