Friday, June 25, 2010

Smashing the Stack: Storing per class information

This stack overflow question demonstrated some issues with storing per class metadata in templated info classes. In particular it boils down to something like this:

Original Code


template<typename T>
struct MetaData
{
  static std::string data;
};
With thre requirement that you add something like this to a source file for every class you require metadata for. Failing to do this will generate a link time error.
std::string MetaData<int>::data;
std::string MetaData<MyClass>::data;
The original code using this kind of method would look something like this:
template <class T>
static void OpenLib(lua_State* L)
{
  if (MetaData<T>::myTable && MetaData<T>::myTableName)
  {
    luaL_openlib(L, MetaData<T>::myTableName, MetaData<T>::myTable, 0);
  }
}

Template sepcialisation

Personally I don't like link time errors and prefer compile time errors, so we could try using template specialisation to get a similar result.
template<typename T>
struct MetaData
{
};
template<>
struct MetaData<int>
{   static std::string data() { return "foo"; }
}
template<>
struct MetaData<int>
{   static std::string data() { return "bar"; }
}
This time you'll get a compile time failure if you fail to define the MetaData for a class that you are using. However there's still a lot of boilerplate code, which I don't like. Secondly the values can't be changed at runtime, which was a requirement of the original post. The code would then look something like this:
template <class T>
static void OpenLib(lua_State* L)
{
  if (MetaData<T>::myTable() && MetaData<T>::myTableName())
  {
    luaL_openlib(L, MetaData<T>::myTableName(), MetaData<T>::myTable(), 0);
  }
}

Singleton

Another trick we can use to avoid needing to define static variables in a .cpp file is to use metadata that uses singleton functions for each variable. Then the function static variable is created inside the function, (on first function use I think) and lives until the end of the application.
template<typename T>
struct MetaData
{
  static std::string& data()
  {
    static std::string data_("");
    return data_;
  }
};
As we return a non-const reference we can still change the value at run-time, like this:
MetaData<int>::data() = "Omg Froobles";
However some people, (myself included) dont really like the use of singletons (or global variables) as they can make code quite hard to test. The code would then look something like this:
template <class T>
static void OpenLib(lua_State* L)
{
  if (MetaData<T>::myTable() && MetaData<T>::myTableName())
  {
    luaL_openlib(L, MetaData<T>::myTableName(), MetaData<T>::myTable(), 0);
  }
}

(Almost) Template free type map.

There's an alternative to using templates as a way into the C++ type system, and that is type_info (Well there's also polymorphism, but we'll ignore that option for now). If we go down that route we can reduce the usage of templates and global/singleton variables significantly.
struct ClassMetaData
{
  std::string data;
};

struct TypeInfoCompare
{
  bool operator()(const std::type_info* i1, const std::type_info* i2) const
  {
    return i1->before(*i2);
  }
};

std::map<const std::type_info*, ClassMetaData, TypeInfoCompare> metaDataMap;
Using this design I'd probably produce code that looks like this:
typedef std::map<const std::type_info*, ClassMetaData, TypeInfoCompare> MetaDataMap;
template<typename T>
static void OpenLib( const MetaDataMap & metadata, lua_State* L)
{
  return OpenLibCore( metadata[ &typeinfo(T) ] );
}
static void OpenLibCore( ClassMetaData & m, lua_State *L )
{
  if ( m.myTable && m.myTableName )
  {
    luaL_openlib(L, m.myTableName, m.myTable, 0);
  }
}
And while you now need to pass in an additional parameter to the OpenLib function, these functions are now easily testable. And if you really hate that additional member you can make the MetaDataMap a global variable or singleton.