Problem: I want disparate sections of my code to be able to access a common collection that stores objects of different types in such a way that the type of each object is known and, crucially, retrieval from the collection should be type checked at compile time. (I realise this is close to questions asked before, but please read on, this is somewhat more specific.)
To give a concrete example, I would like something that does the following:
// Stuff that can go in the collection:
enum Key { NUM_APPLES /* (unsigned int) */, APPLE_SIZE /* (double) */ }
map<Key, Something> collection;
unsigned int * nApples = collection.find(NUM_APPLES);
int * appleSize = collection.find(APPLE_SIZE); // COMPILATION ERROR - WRONG TYPE
My solution: So far I have devised the following solution using boost::any:
The key:
using namespace std;
using namespace boost::any;
struct KeySupertype
{
protected:
// Can't create an instance of this type
KeySupertype() {}
private:
// Can't copy
KeySupertype& operator = (const KeySupertype& other) {}
KeySupertype(const KeySupertype& other) {}
};
template <typename Type>
struct Key : public KeySupertype
{
public:
Key() {}
};
The collection:
class PropertiesMap
{
public:
template<typename T>
T * find(Key<T> & key);
/* Skipping erase, insert and other methods for brevity. */
private:
map<const KeySupertype *, any> myAnyMap;
};
template <typename T>
T * PropertiesMap::find(Key<T> & key)
{
const map<const KeySupertype *, any>::iterator it = myAnyMap.find(&key);
if(it == myAnyMap.end())
return NULL;
return any_cast<T>(&it->second);
}
Usage:
static const Key<unsigned int> NUM_APPLES;
static const Key<double> APPLE_SIZE;
PropertiesMap collection;
/* ...insert num apples and apple size into collection ...*/
unsigned int * const nApples = collection.find(NUM_APPLES);
int * const nApples = collection.find(NUM_APPLES); // COMPILATION ERROR
This way type information is encoded with each Key according to its template parameter so the type will be enforced when interacting with the collection.
Questions:
1) Is this a reasonable way to achieve my goal?
2) A point of nastyness is that the collection uses the address of Key objects as the internal std::map key. Is there a way around this? Or at least a way to mitigate misuse? I’ve tried using a unique int in each Key that was generated from a static int (and making the std::map key type an int), but I’d like to avoid statics if possible for threading reasons.
3) To avoid using boost::any would it be reasonable to have the std::map be of type <const KeySupertype *, void *> and use a static_cast<T> instead of any_cast?
1) Looks nice to me, a clever solution
2) I guess you’re afraid that someone will copy the key and its address will change. If that’s your concern, keep an “original address” field in your
KeySuperType. During construction set the original address to this, during copying set the original address to the original address of the right hand (source). Use this original address to access the map contents. I really couldn’t think of a compile time solution to this, since at compile time, compilation units will not know about each other. You could assign unique ID’s to the keys earliest in link time, and getting the address of global variables is sort of equivalent to that. The only weak point I can see with this solution is if you define the same key in two dynamic shared libraries withoutextern, they’ll silently have their own versions of the key with different addresses. Of course, if everything goes into the same binary, you won’t have that problem, since two declarations withoutexternwill cause a “Multiple Declaration” linker error.3) If your problem with
boost::anyis depending on boost (which is more OK then you think), then implementanyyourself, it’s surprisingly simple. If the problem is performance, thenstatic_cast<>also seems OK to me, as long as you keep the internals of yourPropertiesMapaway from those that don’t know what they’re doing…