Friday, 3 February 2012

Declaring and using C++ properties like in Delphi or C#

I find properties a marvelous concept. You gain so much more control over class members not to mention declaration of the stuff places everything in just a few lines of code. It is immediately visible just by looking at class declaration what the class offers you and what it doesn't.
Anybody who switched from Delphi to C++ will know what I'm talking about.

I have been looking for a way to support properties in C++ for a while now without much luck. Looking at the implementations found on the net, there are two types of implementation:
  1. Property class that stores getter / setter / lvalue pointers
  2. subclasses with pre-processor macro-style implementation
Each of the methods used has its merits and shortfalls. I personally don't like the first approach because it means that for each property declared you waste 3x4 bytes (32 bit) or even 3x8 bytes (64 bit). Plus there's the initialization code setting those pointers adding just about twice the memory consumption on top of the pointers allocated. And I don't like losing so much memory for something as simple as that.
The second approach is a lot more complex to nail down, but once you have the macros, usage is just as easy as the first approach and there is only one detail you need to watch out for: empty class members in C++ take 1 byte of storage (by standard) just for indexing purposes. You can gain this one byte back simply by storing the property l-value inside.

My original intent was to get this thing as far as Delphi's TObject was, but I fell short of implementing the storage / identification subsections. This way you can't do the following two things:
  1. myInstance.setProperty("propertyname", value)
  2. stream << myInstance, stream >> myInstance
So if you feel up to it, I'm all inbox for ya :) I will even explain how it can be done further down.

As explained I decided for the second approach with macros and subclasses because it is much more efficient memory usage-wize. Originally it was intended for there to be only one macro which would do it all, but it turns out C++ preprocessor is pretty dumb and doesn't really allow you to do such things. So I had to split the code in multiple macros, the differences in usage being explained around line 80. Please forgive me for the messed up code, this really wasn't easy to achieve.

At the end of the code there's also the TObject class declared showing most of the functionality off. You can use that class as an example for your usage or use it as the base class for your library. It's up to you.

As mentioned above, I failed to implement storage / load. Turns out my C++ still isn't up to par though this is the most complete property implementation I know of in the net. Only Qt library manages to surpass this implementation, but they have their own preprocessor. Anyway, this code will compile both on MS VC and gcc.

In order to implement that, a global std::map would ned to be declared into which properties would register themselves in their constructors. The key would be class + property and data would be pointers to getter, setter, default and store method pointers. Additionally, each property type would need conversion to string done in order to be able to store classes into XML or something. Having all this implemented, it is then trivial to write getProperty and setProperty functions as well as properly implement saveToXML / loadFromXML.

Anyway, I still feel this implementation of C++ property support is pretty awesome in comparison to other attempts and it should be posted to the almighty net for your pleasure under modified BSD license. The code compiles under MS VC++ and gcc. Have fun.


#include <string>
#include <sstream>
#include <iostream>
#include <typeinfo>
#include <list>

typedef std::wstring vString;

//Support macros begin
//getter macros
#define PROPERTY_GETTER(getter, prop_type, prop_my_struct) \
operator prop_type() { return getter; }                    \
friend std::wostream& operator << (std::wostream &out, prop_my_struct aValue) { out << (prop_type)aValue; return out; }

#define V_GETTER(varName)           getBase()->varName
#define F_GETTER(funcName)          getBase()->funcName()
#define S_GETTER(varName)           varName
#define T_GETTER(varName, funcName) funcName
#define N_GETTER 

//Setter macros
#define PROPERTY_SETTER(setter, prop_type, param_type, prop_my_struct, streamcode) \
prop_my_struct operator = (param_type aValue) { setter; return *this; }            \
friend std::wistream& operator >> (std::wistream &in, prop_my_struct aValue) { streamcode return in; }

#define V_SETTER(varName)  getBase()->varName = aValue
#define F_SETTER(funcName) getBase()->funcName(aValue)
#define S_SETTER(varName)  varName = aValue
#define T_SETTER(funcName) funcName(aValue)
#define N_SETTER 

//Default values
#define DEFAULT_EXPAND(prop_type, value) prop_type(value)
#define N_DEFAULT
//Internal storage (saves one byte for struct default allocation size)
//use N_STORE if you have external storage or no storage
#define STORAGE_EXPAND(prop_type, value) private: prop_type value; public:
#define N_STORE __intstore_func() {}
#define STORAGE_STD(type, defaultval, store)                                   \
bool isDefault() { return (type)(*this) == DEFAULT_EXPAND(type, defaultval); } \
bool stored() { return store; }
#define STORAGE_STDC(type, defaultval, store)      \
bool isDefault() { return (type)(*this) == NULL; } \
bool stored() { return store; }
#define STORAGE_STDS(type, defaultval, store) \
bool isDefault() { return value == NULL; }    \
bool stored() { return store; }

//Constructor / destructor
#define PROPERTY_CTOR(name, set_default) property_##name##_class() { getBase()->registerProperty(L#name); set_default}
#define PROPERTY_DTOR(name, del_obj) ~property_##name##_class() { del_obj }

#define PROPERTY_ALL(base, name, type, ctor, dtor, storage, getfunc, setfunc, defstore, addcode)  \
struct property_##name##_class                                                        \
{                                                                                     \
  ctor                                                                                \
  dtor                                                                                \
  storage                                                                             \
  getfunc                                                                             \
  setfunc                                                                             \
  base *getBase() { return ((base *) ((char *) this - offsetof( base, name ))); }     \
  defstore                                                                            \
  friend class base;                                                                  \
private:                                                                              \
  addcode                                                                             \
} name
//Support macros end

/*
  Note that I had to do some compromises because C++ preprocessor is so dumb
  I found it impossible to support various data types in one macro, so I had to use multiple
  The initial letter before PROPERTY designates a particular data type I supported with that macro
   PROPERTY - standard ordinal type property
  CPROPERTY - property that supports class data types. The macro will support -> operator which 
              accesses class members directly
  SPROPERTY - same as CPROPERTY, but manages allocation of the value (class instance)
  IPROPERTY - supports container types

  The letter trailing PROPERTY designates the use of getter / setter
  _PROPERTY  - property with both a getter and a setter
  _PROPERTYG - property with just a getter (read-only property)
  _PROPERTYS - property with just a setter (write-only property)
*/

//Standard basic type properties
#define PROPERTY(base, name, type, storage, getfunc, setfunc, defaultval, store, addcode)  \
PROPERTY_ALL(base, name, type,                                                             \
             PROPERTY_CTOR(name, ),                                                        \
             PROPERTY_DTOR(name, ),                                                        \
             STORAGE_EXPAND(type, storage),                                                \
             PROPERTY_GETTER(getfunc, type, property_##name##_class &),                    \
             PROPERTY_SETTER(setfunc, type, const type &, property_##name##_class &, in >> aValue;), \
             STORAGE_STD(type, defaultval, store), addcode)

#define PROPERTYG(base, name, type, storage, getfunc, addcode)                             \
PROPERTY_ALL(base, name, type,                                                             \
             PROPERTY_CTOR(name, ),                                                        \
             PROPERTY_DTOR(name, ),                                                        \
             STORAGE_EXPAND(type, storage),                                                \
             PROPERTY_GETTER(getfunc, type, property_##name##_class &),                    \
             ,, addcode)

#define PROPERTYS(base, name, type, storage, setfunc, addcode)                             \
PROPERTY_ALL(base, name, type,                                                             \
             PROPERTY_CTOR(name, ),                                                        \
             PROPERTY_DTOR(name, ),                                                        \
             STORAGE_EXPAND(type, storage),                                                \
             ,                                                                             \
             PROPERTY_SETTER(setfunc, type, const type &, property_##name##_class &, in >> aValue;), \
             , addcode)

//class / struct type properties (note that properties can only be pointers to actual l-values)
//!!!Note that below code assumes that CPROPERTY has storage :( Since I don't know better I may have to re-declare the below 3 macros such that I have one set for initialized storage and one for no storage
#define CPROPERTY(base, name, type, storage, getfunc, setfunc, store, addcode)                  \
PROPERTY_ALL(base, name, type *,                                                                \
             PROPERTY_CTOR(name, storage = NULL;),                                              \
             PROPERTY_DTOR(name, ),                                                             \
             STORAGE_EXPAND(type *, storage),                                                   \
             PROPERTY_GETTER(getfunc, type *, property_##name##_class)                          \
             type * operator ->() { return (type *)(*this); },                                  \
             PROPERTY_SETTER(setfunc, type *, type *, property_##name##_class, in >> aValue;),  \
             STORAGE_STDC(type *, NULL, store), addcode)

#define CPROPERTYG(base, name, type, storage, getfunc, addcode)                                \
PROPERTY_ALL(base, name, type *,                                                               \
             PROPERTY_CTOR(name, ),                                                            \
             PROPERTY_DTOR(name, ),                                                            \
             STORAGE_EXPAND(type *, storage),                                                  \
             PROPERTY_GETTER(getfunc, type *, property_##name##_class)                         \
             type * operator ->() { return (type *)(*this); },                                 \
             ,, addcode)

#define CPROPERTYS(base, name, type, storage, setfunc, addcode)                                \
PROPERTY_ALL(base, name, type *,                                                               \
             PROPERTY_CTOR(name, ),                                                            \
             PROPERTY_DTOR(name, ),                                                            \
             STORAGE_EXPAND(type *, storage),                                                  \
             ,                                                                                 \
             PROPERTY_SETTER(setfunc, type *, type *, property_##name##_class, in >> aValue;),, addcode)

#define SGET_FUNC(type) *(value == NULL?value = new type():value)
#define SSET_FUNC(type) if (value == NULL) value = new type(aValue); else *value = aValue
#define SPROPERTY(base, name, type, store, addcode)                          \
PROPERTY_ALL(base, name, type *,                                             \
             PROPERTY_CTOR(name, value = NULL;),                             \
             PROPERTY_DTOR(name, if (value != NULL) delete value;),          \
             STORAGE_EXPAND(type *, value),                                  \
             PROPERTY_GETTER(SGET_FUNC(type), type, property_##name##_class &) \
             type operator ->() { return (type)(*this); },                   \
             PROPERTY_SETTER(SSET_FUNC(type), type, type, property_##name##_class &, in >> aValue;), \
             STORAGE_STDS(type, NULL, store), addcode)


//indexed properties that would map to a get() or set() function (but not both) are impossible
//  since both [] and () operators can only be overloaded such that they return reference to the element in question
//  this would mean you actually have to have data behind the get() / set() function, which is not the purpose 
//  of such properties thus I decided not to support indexed properties! :(
//  instead I support:
//Container (array) properties
//  These are simplified class properties that store the container pointer. The pointer is allocated only upon access
//  Read only access is supported (can't assign a different instance to the property)
//  To avoid unnecessary allocation, a bool empty() method is provided which returns whether the object is instantiated
//  This is done so because std::map takes 28 bytes and std::list takes 24 bytes in 32-bit MSVC
//  I prefer 4 (32 bit) / 8 (64 bit) bytes of fixed storage per class instance
//  These properties are always read/write though meaning you can access ALL container methods / operators once instantiated. Sorry :)
//  In order to be able to load / store these properties (main intent for their support), you must supply the following:
//    begin() and end() iterator methods as implemented in std:: container classes
//    << and >> operator overloads for the container types
#define AGET_FUNC(type) (value == NULL?value = new type():value)
#define ASET_FUNC(type) if (value == NULL) value = new type(); value = aValue
#define ASTORAGE(store)            \
bool isDefault() { return false; } \
bool stored() { return store; }

#define IPROPERTY(base, name, type, store, addcode)                                               \
PROPERTY_ALL(base, name, type *,                                                                  \
             PROPERTY_CTOR(name, value = NULL;),                                                  \
             PROPERTY_DTOR(name, if (value != NULL) delete value; ),                              \
             STORAGE_EXPAND(type *, value),                                                       \
             PROPERTY_GETTER(AGET_FUNC(type), type *, property_##name##_class)                    \
             type * operator ->() { return (type *)(*this); },                                    \
             PROPERTY_SETTER(ASET_FUNC(type), type *, type *, property_##name##_class, in >> aValue;),  \
             ASTORAGE(store), addcode)

class Object
{
public:
  Object() {}
  Object(const Object &aValue) {}
  virtual ~Object() { parent = NULL; } 

  virtual Object *create() const = 0; //"virtual default constructor"
  Object *create(Object *aParent) { Object *res = create(); res->parent = aParent; return res; } 
  virtual Object *copy() { Object *obj = create(); obj->copyMembers(); return obj; } //"virtual" copy constructor
  virtual void copyMembers() {}

   PROPERTYG(Object, className, vString, N_STORE, F_GETTER(getClassName),);
  SPROPERTY (Object, name,      vString, true, );
  CPROPERTY (Object, parent,    Object,  mParent, S_GETTER(mParent), F_SETTER(setParent), false,);
  IPROPERTY (Object, children,  std::list<Object *>, true,);
   PROPERTY (Object, tag,       int, value, S_GETTER(value),   S_SETTER(value), 0, true, ); //for some reason MSVC 2008 increases the object by 20 bytes if I use an int64 here :(

protected:
  vString getClassName() 
  {
    std::string s(typeid(*this).name());
    return vString(s.begin(), s.end()); 
  }

  void children_append(const Object *aParent);

  void setParent(Object *aParent)
  {
    //First remove any existing parent
    if (parent != NULL)
      parent->children->remove(this);
    parent.mParent = aParent;
    //Add me to the new parent's children list
    if (aParent != NULL)
      parent->children->push_back(this);
  }

  static void registerProperty(vString name) {} //TODO: Do the code, man
  static void registerClass() {} //TODO: Do the code, man

  vString getProperty(vString aPropertyName) {} //TODO: Do the code, man
  void setProperty(vString aPropertyName) {} //TODO: Do the code, man

  friend std::wostream& operator << (std::wostream &out, Object &aValue) { /*out << aValue;*/ return out; }
  friend std::wistream& operator >> (std::wistream &in,  Object &aValue) {  }
};

No comments: