Circular Dependencies in C++ Template Classes

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.

SDL 2.0, OpenGL, and Multithreading (Windows)

SDL makes working with OpenGL fairly convenient, but setting up a separate rendering thread can be a bit tricky.  OpenGL contexts by default are not very multithread-friendly, either, as they are very tightly bound to the thread that created them.  The considerations:

  • We want a separate thread just for rendering, so SDL events should be available to the main thread.
  • We want the main thread to spawn the rendering thread, not the other way around
  • We want the graphics to actually show up

Start off in the main thread by initializing video:

if (SDL_Init(SDL_INIT_VIDEO) < 0) {
error("Error on SDL_Init: ", SDL_GetError());
return false;
}

Set your GL attributes.  The important one for our purposes is in bold:

SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);

Now open a window (using the SDL_WINDOW_OPENGL option, naturally):

_sdlWindow = SDL_CreateWindow("SDL Rocks", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1024, 768, SDL_WINDOW_OPENGL);

If we don’t create the window in the main thread, we no longer have access to SDL events like mouse, keyboard, etc from the main thread.

Now we’re going to make two GL contexts. The second one will be “current” since it is created after the first.

_sdlGlContext = SDL_GL_CreateContext(_sdlWindow);
_sdlInitContext = SDL_GL_CreateContext(_sdlWindow);

Now it’s time to spawn our rendering thread:

_renderThread = SDL_CreateThread(Renderer::renderThread, "renderThread", NULL);

In the render thread now, we need to make the other context current:

SDL_GL_MakeCurrent(_sdlWindow, _sdlGlContext);

(Try this without the second context, and just make the first context current from the new thread.  It does not work. Why? Reasons.)

Now that we have this context current, we can finally set up some parameters:

glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

If you did this under the first context, the settings will be lost, so initialize them once you’re in the render thread. Now you can start your render loop as usual.

I will leave the business of thread synchronization to you!  Best of luck with that.