Coming from a Java background, perhaps my favorite aspect of C++ is the flexibility of templates and their ability to provide a robust alternative to dynamic polymorphism. However, since their implementation is limited to header files, they are prone to dependency issues. Consider the following example (for clarity purposes I’ve removed some type magic necessary for vector storage):
//in "EventListener.hpp"
include "EventProcessor.hpp" //required!
template<typename EVENT_TYPE>
struct EventListener {
EventListener() {
EventProcessor::instance()->registerListener(this);
}
virtual void handleEvent(EVENT_TYPE& e) = 0;
}
//in "EventTopic.hpp"
include "EventListener.hpp" //required!
template<typename EVENT_TYPE>
struct EventTopic {
vector<EventListener*> listeners;
void registerListener(EventListener* listener) {
//add to listeners
}
void processEvent(EVENT_TYPE& e) {
for(auto listener : listeners) {
listener->handleEvent(e);
}
}
}
//in "EventProcessor.hpp"
#include "EventTopic.hpp" //required!
struct EventProcessor {
vector<EventTopic*> topics;
template <typename EVENT_TYPE>
void registerListener(EventListener* listener) {
//add listener to topic of EVENT_TYPE
}
template <typename EVENT_TYPE>
void processEvent(EVENT_TYPE& e) {
//find topic for this event type
topic->processEvent(e);
}
}
We have an event processor that maintains a list of topics per event type. Each topic has a list of event listeners that will consume events of that type. On construction, listeners will register themselves with the processor.
It should be fairly clear here that EventListener.hpp
includes EventProcessor.hpp
includes EventTopic.hpp
includes EventListener.hpp
. Yet all of the structs require full definitions of the included structs.
It’s not immediately apparent that template classes and functions can be forward-declared and then implemented later in the file. It can look quite messy, but it works. The previous example must be implemented entirely in one file using this strategy:
//in "EventHandlingGiantHeader.hpp"
//forward declare EventListener
template <typename EVENT_TYPE>
struct EventListener;
//implement EventTopic, but forward declare functions
template <typename EVENT_TYPE>
struct EventTopic {
vector<EventListener<EVENT_TYPE>*> listeners;
void registerListener(EventListener<EVENT_TYPE>* listener);
void processEvent(EVENT_TYPE& e);
}
//implement EventProcessor, but forward declare functions
struct EventProcessor {
vector<EventTopic*> topics
template <typename EVENT_TYPE>
void registerListener(EventListener* listener);
template <typename EVENT_TYPE>
void processEvent(EVENT_TYPE& e);
}
//implement EventListener
template <typename EVENT_TYPE>
struct EventListener {
EventListener() {
EventProcessor::instance()->registerListener(this);
}
virtual void handleEvent(EVENT_TYPE& e) = 0;
}
//implement EventTopic functions
template <typename EVENT_TYPE>
void EventTopic<EVENT_TYPE>::registerListener(EventListener<EVENT_TYPE>* listener) {
//add to listeners
}
template <typename EVENT_TYPE>
void EventTopic<EVENT_TYPE>::processEvent(EVENT_TYPE& e) {
for(auto listener : listeners) {
listener->handleEvent(e);
}
}
//implement EventProcessor functions
template <typename EVENT_TYPE>
void EventProcessor::registerListener(EventListener<EVENT_TYPE>* listener) {
//add listener to topic of EVENT_TYPE
}
template <typename EVENT_TYPE>
void EventProcessor::processEvent(EVENT_TYPE& e) {
//find topic for this event type
topic->processEvent(e);
}
One important limitation is that you cannot implement constructors or destructors as template functions.
This technique can lead to some bloated header files, and circular dependencies in general are to be avoided anyway. But there are certain situations where it does make sense, and luckily it is possible.