I have questioned the sanity of such approach before. But if well defined, and carefully coded, it can work.
Personally, I once used such approach, but I used templates to specify return types, resulting in compile-time checks to avoid many problems.
To use fully run-time evaluated system, this is how I would approach it (using a proxy holder for return types).
#include <iostream>#include <string>#include <map>struct JType { JType() { std::cout << "JType()" << std::endl; } virtual ~JType() { std::cout << "~JType()" << std::endl; } virtual void commit() = 0;};struct JDefault : public JType { JDefault() { std::cout << "JDefault()" << std::endl; } ~JDefault() { std::cout << "~JDefault()" << std::endl; } void commit() { std::cout << "Commit not supported" << std::endl; }};struct JString : public JType { JString() { std::cout << "JString()" << std::endl; } ~JString() { std::cout << "~JString()" << std::endl; } void commit() { std::cout << "Hello World" << std::endl; }};class Object { typedef JType * T; typedef std::map<std::string, T> List; // non-copyable, too messy to implement Object(const Object &) {} Object operator=(const Object &) {} struct Proxy { friend class Object; Proxy & operator=(T rhs) { clear(); ref = rhs; return *this; } bool exists() const { return ref != NULL; } void clear() const { delete ref; ref = NULL; } T operator->() const { return value(); } T operator*() const { return value(); } private: Proxy(const List::iterator & i) : ref(i->second) {} Proxy(const Proxy & other) : ref(other.ref) {} T value() const { static JDefault def; return (ref == NULL) ? &def : ref; } // assignment can't be implemented Proxy & operator=(const Proxy &) { } T & ref; }; List list;public: Object() {} ~Object() { List::iterator i = list.begin(); for (List::iterator i = list.begin(); i!= list.end(); ++i) delete i->second; } Proxy operator[](const std::string & key) { List::iterator i = list.find(key); if (i != list.end()) { return Proxy(i); } else { return Proxy(list.insert(list.end(), std::make_pair(key, T(NULL)))); } } bool has(const std::string & key) { return list.count(key) != 0; }};int main(int argc, char* argv[]){ Object o; o["math"] = new JString(); if (o["empty"].exists()) std::cout << "'empty' should not exist" << std::endl; o["math"]->commit(); o["math"].clear(); o["math"]->commit(); o["empty"]->commit();}
I have tried to think of all the corner cases while limiting the code to nothing and absolutely nothing beyond the given problem. Even with that I'm not sure I got it right (C++ is annoying in that way).
In above case Proxy is temporary holder for reference to actual pointer which resides inside std::map's node. Proxy should be transient object that cannot persist beyond scope of caller. It also relies on map not being changed while its alive.
In release builds, Proxy is completely eliminated, all operations on it result in query of actual holder of pointer.
Lastly, there's the performance penalty to consider. Each time a variable is accessed, map must be searched. This could be solved by allowing Proxy to be cached, but that throws safety out of window.
Finally: std::map is designed the way it is for a good reason. Unlike managed objects, where NULL/null is a given, map[] may result in undefined behavior. It is almost without exception better to use the map properly rather than trying to go against the design of C++.
Also - Python, PHP, Lua or similar would be much better choice to implement system like this. C++ is simply the wrong language for this.