Crog
Crog

Reputation: 1170

Recursive Template List instead of hard coded switch statement with key/id based search

I am reading/writing a structure using C++ (vc2008) whose type changes based on an ID flag obviously at runtime. The creation of the correct type and/or reading and writing would require a switch. The closest existing example is Using template instead of switch but this does not allow the type to be specified at run-time. To avoid creating the same switch in multiple places I have been investigating the use of recursive templates to overcome this issue. This is my first time with these so there may be some major improvements that can be done to the code example!

Below is a working example. As you will see in 'main()' the type id used is a variable int which can be set to any runtime value. Calling a function on TypeList<> will recurse through the types until it reaches a matching ID or a void type.

#include <stdio.h>
#include <iostream>

//Base type
struct Base
{
    //NOTE: The virtual destructor can be added  to aid with debugging
    //virtual ~Base(){}

    friend std::ostream& operator << ( std::ostream& stream, const Base& rhs )
    { return stream << "Base";  }
};

struct A : Base
{
    friend std::ostream& operator << ( std::ostream& stream, const A& rhs )
    { return stream << "A";  }
};

struct B : Base
{
    friend std::ostream& operator << ( std::ostream& stream, const B& rhs )
    { return stream << "B";  }
};

struct C : Base
{
    friend std::ostream& operator << ( std::ostream& stream, const C& rhs )
    { return stream << "C";  }
};

//Recursive template type
// - If the ID/key does not match the next type is checked and so on
template < unsigned int kID, typename _Type, typename _TNext >
struct TypeList
{
    typedef _Type Type;
    typedef typename _TNext::Base Base;

    static Base* doNew( unsigned int id )
    { return id == kID ? new _Type() : (Base*)_TNext::doNew(id); }

    static void doDelete(unsigned int id, Base* rhs )
    { id == kID ? delete (_Type*)rhs : _TNext::doDelete(id, rhs ); }

    static std::ostream& doWrite( unsigned int id, std::ostream& stream, const Base* rhs ) 
    { return id == kID ? stream << (*(const _Type*)rhs) : _TNext::doWrite(id, stream, rhs); }
};

//Specialise the 'void' case to terminate the list
// TODO; this doesn't seem as elegant as possible!? How can we separate the logic from the functionality better...
template < unsigned int kID, typename _Type >
struct TypeList<kID, _Type, void>
{
    typedef _Type Type;
    typedef _Type Base;

    static _Type* doNew( unsigned int id )
    { return id == kID ? new _Type() :0; }

    static void doDelete(unsigned int id, _Type* rhs )
    { if ( id == kID ) delete rhs; }

    static std::ostream& doWrite( unsigned int id, std::ostream& stream, const _Type* rhs ) 
    { return id == kID ? stream << (*(const _Type*)rhs) : stream; }
};

// ID values used to identify the different structure types
enum eID
{
    ID_A,
    ID_B,
    ID_C,
};

//Create our ID and Type list
typedef TypeList<   ID_A,   A,
    TypeList<       ID_B,   B,
    TypeList<       ID_C,   C, 
    TypeList<       -1 ,    Base,       void> > > > TypesList;

int _tmain(int argc, _TCHAR* argv[])
{
    eID type = ID_C; //, We are dealing with a type of 'C'  
    Base* newInst = TypesList::doNew( type );   //Create a new C
    TypesList::doWrite( type, std::cout, newInst ); //Write 'C' to the console  
    TypesList::doDelete( type, newInst );   //Delete C
    return 0;
}

What are peoples views on this and other/better ways to do this? Mainly is there a way to nicely separate the logic from the functionality of the class to save having duplicated code in the TypeList<,,_Type> and the TypeList<,,void> instantiations.

EDIT: The solution preferably requires no run-time setup to 'add' types to a lookup or the likes.

Cheers, Craig

Upvotes: 2

Views: 529

Answers (1)

Ylisar
Ylisar

Reputation: 4291

This solution has a number of disadvantages which makes it sub optimal in my mind. Most of it comes down to the fact that the TypeList becomes a major compilation bottleneck, in the same way the switch case would've been. In my experience doWrite / doDelete in this example is better solved by virtual dispatch, but the actual object creation needs the mapping of runtime data to the concrete type. The best solution for this, imo, is to just go for a factory. If you have Loki it's as simple as:

// BaseFactory.h
typedef Loki::SingletonHolder< Loki::Factory< Base, std::string > > BaseFactory;
#define REGISTER_BASE_FACTORY( x ) \
static bool BOOST_PP_CAT( registerBaseFac, x ) = BaseFactory::Instance().Register( BOOST_PP_STRINGIZE( x ), boost::phoenix::new_< x >() );

// For example A.cpp
REGISTER_BASE_FACTORY( x );

// Somewhere else
...
Base* someInstance = BaseFactory::Instance().CreateObject("A");
assert( typeid( *someInstance ) == typeid( A ) );
...

Personally I use a different factory base which is more akin to:

#pragma once
#include "boost/unordered_map.hpp"
#include <cassert>

template< typename KeyType, typename ProductCreatorType >
class Factory
{
    typedef boost::unordered_map< KeyType, ProductCreatorType > CreatorMap;
public:
    const ProductCreatorType& operator()( const KeyType& a_Key ) const
    { 
        typename CreatorMap::const_iterator itrFnd = m_Creators.find( a_Key ); 
        assert( itrFnd != m_Creators.end() );
        return itrFnd->second;
    } 
    ProductCreatorType& operator()( const KeyType& a_Key )
    { 
        typename CreatorMap::iterator itrFnd = m_Creators.find( a_Key ); 
        assert( itrFnd != m_Creators.end() );
        return itrFnd->second;
    } 
    bool RegisterCreator( const KeyType& a_Key, const ProductCreatorType& a_Creator )
    {
        return m_Creators.insert( std::make_pair( a_Key, a_Creator ) ).second;
    }
private:
    CreatorMap m_Creators;
};

Simply because of the fact that it's more flexible ( able to handle for example returning boost::shared_ptr<> ).

The major advantage of this approach is that you can have the registration code in the same translation unit as the concrete type. Way easier to separate client & library code and modifying a concrete type doesn't cause recompilation of everything that needs the factory. As a bonus the performance scale is better as well.

If you don't want virtual dispatch, you can use the same approach, but instead use member function pointers and supply the instance, this can be solved with an almost identical approach using boost::bind.

EDIT: So yeah, missed that you wanted it completely compile time based, although to be honest I don't quite see any benefits.

Upvotes: 1

Related Questions