Jump to content

  • Log In with Google      Sign In   
  • Create Account

Banner advertising on our site currently available from just $5!


1. Learn about the promo. 2. Sign up for GDNet+. 3. Set up your advert!


Like
11Likes
Dislike

How to Handle Circular Dependencies with Templates in C++

By Kent Fagerjord | Published Jan 16 2014 11:10 AM in General Programming
Peer Reviewed by (Dave Hunt, Josh Vega, Dragonsoulj)

c++ templates circular dependency entity component

Although templates in C++ are well known, and more and more people actually do know how to wrap their head around them, there are a couple of less-common circumstances which can leave the programmer rather clueless. This is a problem I encountered while tuning the Message-Component-Entity system in our game under development, Burnt Islands.

The problem


In the C++ language, there are not many features that are as powerful as templates. But their strength is also contributing to the complexity of templates.

With standard classes and structs, you can hide the implementation of the methods within the class, and you can put those in separate compilation units (cpp files).

With templates, there is a whole other story. When the compiler sees a template (class and method), it must know the internals of that particular method. This is because templates doesn't really exist until they are used, and with varying template parameters, the classes and structs are generated, and is in reality completely separate classes for different parameters.

Here is one quirk: circular dependencies with templated classes and classes with templated methods. All code in the post (and build scripts) is available at https://github.com/Studiofreya/code-samples/tree/master/circular-templates.

In this particular case, Visual Studio is sort of stupid. It deliberately accepts code which it really shouldn't accept. This isn't really a problem if you only target the Microsoft stack and environment. If you plan to deploy to other platforms, it'll be a problem. The best advice is to get up both build systems early in the development, so any problems are caught early.

Code (problematic)


But on with the problem. Consider the following file.

// This file only compiles in Visual Studio 2010 newer

template<typename T>
class Message
{
    public:
        typedef T       type;

        static const int MsgId   = 0;


        explicit Message( const T & t )
            : data(t)
        {
        }


        type        data;
};

typedef Message<int>    MsgInt;
typedef Message<long>   MsgLong;
typedef Message<float>  MsgFloat;
typedef Message<double> MsgDouble;

class MessageHandler
{
    public:

        template<typename MSG, typename T>
        void setHandler( const T & msghandler )
        {
            /* do stuff */
            auto id = MSG::MsgId; // Make sure this is a message type
        }
};


// Forward declaration of classes
class Component;
class Entity;

class Component
{
    public:

        template<typename MSG>
        void setEntityHandler( Entity & entity )
        {
            auto handleMsg = [&] ( const MSG & msg )
            {
                /* do stuff */
            };

            entity.m_MessageHandler.setHandler<MSG>( handleMsg );
        }


        MessageHandler  m_MessageHandler;


};

class Entity
{
    public:


        template<typename MSG>
        void setComponentHandler( Component & component )
        {
            auto handleMsg = [&] ( const MSG & msg )
            {
                /* do stuff */
            };

            component.m_MessageHandler.setHandler<MSG>( handleMsg );
        }

        MessageHandler  m_MessageHandler;
};


int main()
{
    Component c;
    Entity e;

    e.setComponentHandler<MsgDouble>(c);

	return 0;
}


The problem is in this line entity.m_MessageHandler.setHandler( handleMsg ); in the Component class. It uses an undefined class which has been forward declared. Visual Studio is trying to be smart. It can see forwards and there is an implementation (definition) of the Entity class further down below. GCC (g++) is not trying to be smart, and thus the code above fail to compile with g++.

Here is the error with g++.

g++-4.8.1 -std=c++11 circular-templates.cpp -o circdep
circular-templates.cpp: In member function 'void Component::setEntityHandler(Entity&)':
circular-templates.cpp:55:19: error: invalid use of incomplete type 'class Entity'
             entity.m_MessageHandler.setHandler( handleMsg );
                   ^
circular-templates.cpp:41:7: error: forward declaration of 'class Entity'
 class Entity;
       ^
circular-templates.cpp:55:51: error: expected primary-expression before '>' token
             entity.m_MessageHandler.setHandler( handleMsg );
                                                   ^

Clean code (ideal)


Here is the fixed code where it will compile in both MSVC and g++.

// This file compiles in both Visual Studio 2010 (and newer) and G++ with C++11.

template<typename T>
class Message
{
    public:
        typedef T       type;

        static const int MsgId   = 0;


        explicit Message( const T & t )
            : data(t)
        {
        }


        type        data;
};

typedef Message<int>    MsgInt;
typedef Message<long>   MsgLong;
typedef Message<float>  MsgFloat;
typedef Message<double> MsgDouble;

class MessageHandler
{
    public:

        template<typename MSG, typename T>
        void setHandler( const T & msghandler )
        {
            /* do stuff */
            auto id = MSG::MsgId; // Make sure this is a message type
        }
};


// Forward declaration of classes
class Component;
class Entity;

class Component
{
    public:

        template<typename MSG>
        void setEntityHandler( Entity & entity );

        MessageHandler  m_MessageHandler;

};

class Entity
{
    public:


        template<typename MSG>
        void setComponentHandler( Component & component );

        MessageHandler  m_MessageHandler;
};


// Implementation of circular dependencies
template<typename MSG>
void Component::setEntityHandler( Entity & entity )
{
    auto handleMsg = [&] ( const MSG & msg )
    {
        /* do stuff */
    };

    entity.m_MessageHandler.setHandler<MSG>( handleMsg );
}

template<typename MSG>
void Entity::setComponentHandler( Component & component )
{
    auto handleMsg = [&] ( const MSG & msg )
    {
        /* do stuff */
    };

    component.m_MessageHandler.setHandler<MSG>( handleMsg );
}

// Main
int main()
{
    Component c;
    Entity e;

    e.setComponentHandler<MsgDouble>(c);

	return 0;
}


This file will compile in both MSVC and G++, and with MSVC it'll produce identical assembly code for both files.

The clue to solving this problem is to declare both classes before providing the definitions (implementations). It's not possible to split the declaration and definition into separate files, but you can structure them as if they were in separate files. Here both Entity and Component are declared before any implementation code.

In the end, this might or might not be a problem you will encounter. But when you encounter it, you'll know about it and remember there are solutions available.

All code in the post (and build scripts) is available at https://github.com/Studiofreya/code-samples/tree/master/circular-templates.



About the Author(s)


Currently a full time indie developer (previously employed as a C++ system programmer) working on Burnt Islands (http://games.studiofreya.com/).

http://studiofreya.com/


License


GDOL (Gamedev.net Open License)




Comments

Why not optimize and use virtual function pointers.  The events can be handled through a simple array of functions labeled with maybe enum or something.  I never liked event loops.

@codenine75a - The article isn't about event loops. It's about how to avoid circular dependencies with templates in C++.

@codenine75a: As Dave Hunt said, this article was about handling circular dependencies with templates in C++, and thus the code samples are at a bare minimum.

 

I chose to use real world names for the class names, because I personally can't follow advanced code written like "class Foo" and "class Bar".

 

However, I'm planning some articles about messages and how to use implement and use them in a game. I will not make any promises, but I hope to get them done this year :).

@Freya Yeah, it is especially awful when I see code written using "Class A", Class B", "int a"....etc

BTW I did enjoy the article.

Just a note about one comment in the article which is misleading.  "It's not possible to split the declaration and definition into separate files", it is possible, you just have an include after the declarations or include the files in order from another header.  This is fairly common practice with things such as Boost where you put the definition portion of the code in a 'details' directory.  It is not a h versus c file styled break but it does allow you to break the code into logical separate units for well organized declaration and definition portions.

Visual Studio is trying to be smart. It can see forwards and there is an implementation (definition) of the Entity class further down below. GCC (g++) is not trying to be smart, and thus the code above fail to compile with g++.

It's actually kind of the other way around. Visual Studio is still to this day missing two-phase lookup. It works not because it's smart but because it's incorrectly delaying all semantic analysis of the template until instantiation, which while solving issues like this causes other bugs.

You could actually just modify your templates slightly to make GCC and other standards-conforming compilers happy by making the type of Entity a template parameter to the message handler functions (and hence a dependent type).  Then there will be no semantic checks on the entity parameter until instantiation and everything will work.

Just a note about one comment in the article which is misleading.  "It's not possible to split the declaration and definition into separate files", 

 

It's also possible for template specializations, which granted are only used or useful in certain cases.


Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS