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();
}