⬆️ ⬇️

Class fields available by name with setter and getter in C ++

As you know, in C ++ there is no means for describing the fields of a class with controlled access, such as property in C #. On Habrahabr, the article has already partially covered this topic, but I absolutely do not like the syntax. Besides, I really wanted to be able to access the fields from the run-time by name.



If you want to solve a problem, try to find out the answer first.



Let's estimate what you need to get.

For example, an int field with the name "x". We are quite satisfied with this entry:

field(int,x); 


And further in the code we want to access this field.

 foo.x = 10; int t = foo.x; foo.setField("x", 15); int p = foo.getField("x"); 


Sometimes we also want to control the installation and getting the values ​​from this field ourselves, so we’ll also have to write getters and setters.

And you also need not forget about the possibility of field initialization.



Where to begin



What you need to know in the run-time about the fields? At least their names and meanings. And it would not be bad to know the type.



Type of


 class Type { public: const std::string name; const size_t size; template <typename T> static Type fromNativeType() { return Type(typeid(T).name(), sizeof(T)); } Type(const char * name, size_t size) : size(size), name(name) { } Type(const Type & another) : size(another.size), name(another.name) { } }; 


This is far from a complete implementation of the class describing the type. In fact, it is possible and necessary to add a lot more, but for the problem to be solved this is not the most important, and the name and size is quite enough. Perhaps I will write a separate article on the type description.

It seems more or less simple, only the static method confuses. The fact is that the syntax does not allow instantiating the template constructor by passing template arguments in triangular brackets.

Example

 class Bar { public: template <int val> Bar() { int var = val; printf("%d\n", var); } }; 


The Bar class itself is not a template, but it has a default template constructor. So to call this constructor it must be instantiated. This code suggests itself:

 Bar bar = Bar<10>(); 


But such a record means instantiating a template class, not a template constructor.

Sometimes it is possible to bypass it and further I will show how.

Thus Type :: fromNativeType <> () is, in a sense, also a constructor.

')

Field storage



Since we want to refer to the fields by their names from the run-time - we have to store them in some way. I chose the following option: create a base class from which all the others are inherited. This class contains a repository of field information and methods for accessing it.

 class Basic { std::vector<FieldDeclaration> fields; public: template <typename FieldType> FieldType getField(const std::string & name, FieldType default) { for(int i = 0; i < fields.size(); ++i) { if (fields[i].name.compare(name)==0) { return static_cast< Field<FieldType>* >(fields[i].pointer)->getValue(); } } return default; } template <typename FieldType> void setField(const std::string & name, FieldType value) { for(int i = 0; i < fields.size(); ++i) { if (fields[i].name.compare(name)==0) { static_cast< Field<FieldType>* >(fields[i].pointer)->setValue(value); } } } }; 


For storage it is better to use probably std :: map, for example, std :: vector is appropriate.

FieldDeclaration is simply a structure containing type information.

 struct FieldDeclaration { FieldDeclaration(const std::string & name, const Type & type, void * pointer = NULL) : name(name), type(type), pointer(pointer) { } const std::string name; const Type type; void * pointer; }; 




Magic magic



Of course, this whole system was not written the first time, and the most basic part of it was modified many times in general, due to the fact that some of the ways to solve the problem led to a dead end.

Therefore, I will insert only code snippets that come together in the big picture.



Some concepts used


 #define __CONCAT__(a,b) a##b #define __STRINGIZE__(name) #name #define __CLASS_NAME__(name) __CONCAT__(__field_class__, name) #define __GETTER_NAME__(fieldname) __CONCAT__(getterof_, fieldname) #define __SETTER_NAME__(fieldname) __CONCAT__(setterof_, fieldname) 




Pseudo-keyword


At the beginning of the article we agreed that we would use the syntax of the field description, which takes 2 arguments: the type and the name of the field. In fact, I made the separation of two types of fields:



 #define smartfield(type,name) \ type __stdcall __GETTER_NAME__(name)(); \ void __stdcall __SETTER_NAME__(name)(type value); \ __FIELD_CLASS_DECLARATION_SMART__(type,name) \ __CLASS_NAME__(name) name; #define field(type, name) \ __FIELD_CLASS_DECLARATION__(type,name) \ __CLASS_NAME__(name) name; 


The first two lines of the smartfield macro declare the getter and setter of the corresponding field directly in the class where the field will be located. Then you must write their implementation. They will be called getter_ <field name> and setter_ <field name>, respectively.

The __stdcall call agreement modifier allows you to call a class method at a pointer, passing this explicitly as the first parameter (the __thiscall agreement according to the Microsoft specification used by default uses the ECX register to pass this).

__FIELD_CLASS_DECLARATION__ and __FIELD_CLASS_DECLARATION_SMART__ are descriptions of the classes of the corresponding fields (we will return to the “classes of the internal kitchen”).

__CLASS_NAME __ (name) name; it is actually a copy of the "classes of domestic cuisine."



class Field


It should be noted that the "classes of domestic cuisine" are descendants of a more general class Field

 #define NO_GETTER (TGetter)0 #define NO_SETTER (TSetter)0 template <typename FieldType> class Field { protected: typedef FieldType (*TGetter)(void *); typedef void (*TSetter)(void *, FieldType); TGetter getter; TSetter setter; void * that; public: const std::string name; const Type type; FieldType value; template< typename OwnerType > Field(OwnerType * _this, const char * nm) : name( nm ), type( Type::fromNativeType<FieldType>() ), getter(NO_GETTER), setter(NO_SETTER), that(_this) { _this->fields.push_back(FieldDeclaration(name, type, this)); } template< typename OwnerType > Field(OwnerType * _this, const char * nm, const FieldType & initvalue) : name( nm ), type( Type::fromNativeType<FieldType>() ), value(initvalue), getter(NO_GETTER), setter(NO_SETTER), that(_this) { _this->fields.push_back(FieldDeclaration(name, type, this)); } FieldType getValue() { if (getter) return getter(that); else return value; } void setValue(FieldType val) { if (setter) setter(that,val); else value = val; } Field<FieldType> & operator = (FieldType val) { setValue(val); return *this; } operator FieldType() { return getValue(); } }; 


So, we have a template Field class, the template of which requires specifying the field type.

Class store in itself:



Notice that the TGetter and TSetter types are written in such a way that the functions they describe take the pointer void * as the first parameter. This is actually a pointer to that. This works because the getter and setter are clearly marked with the __stdcall modifier.



Now the designers. They are template, the template is parameterized for the owner class types OwnerType, that is, the class in which the field is declared. The constructor itself takes the pointer this of the class OwnerType and saves it to that. By the way, as I have already said, it is impossible to explicitly parameterize a constructor, but templates have an interesting feature: if there is an opportunity to deduce the type by which you need to parameterize a template automatically, then this is what happens. In this case, this is the same situation. When passing this to the constructor, the compiler itself must substitute the type OwnerType.

The nm argument takes the symbolic name of the field. It is created by the stringing operator (see above __STRINGIZE__) from higher macros.

By default, we initialize the getter and setter to zero values ​​so that we know that they should not be called. If the getter and setter are present, they will be set separately in the heir classes.

The difference between the second constructor and the first one is that it accepts the default field value, since It is quite often used.



Next come the default getter and setter. They check for the presence of a getter / setter set by the programmer, and if they are given, call them with an explicit transfer to that first parameter. Otherwise, they simply return the value / assign the new.



The assignment operator and the cast operator are needed simply for syntactically more convenient access to the field value.



Classes of domestic cuisine



 #define __FIELD_CLASS_DECLARATION__(type, name) \ class __CLASS_NAME__(name) : public Field<type> \ { \ public: \ __FIELD_CLASS_CONSTRUCTOR_1__(type,name) \ __FIELD_CLASS_CONSTRUCTOR_2__(type,name) \ __CLASS_NAME__(name) & operator = (type val) \ { \ Field<type>::operator=(val); \ return *this; \ } \ }; #define __FIELD_CLASS_DECLARATION_SMART__(type, name) \ class __CLASS_NAME__(name) : public Field<type>\ { \ public: \ __FIELD_CLASS_CONSTRUCTOR_1_SMART__(type,name) \ __FIELD_CLASS_CONSTRUCTOR_2_SMART__(type,name) \ __CLASS_NAME__(name) & operator = (type val) \ { \ Field<type>::operator=(val); \ return *this; \ }\ }; 


These classes will be substituted directly into the owner class. To unify the name of these classes, use the macro __CLASS_NAME__ (see above). They are all descendants of the Field class already reviewed.

A good practice is to return the reference to itself by the assignment operator, this allows you to write cascade assignments.

All difference between them in designers.



About the constructors of these classes


 #define __FIELD_CLASS_CONSTRUCTOR_1_SMART__(type,name) \ template< class OwnerType > \ __CLASS_NAME__(name)(OwnerType * _this) \ : Field<type>(_this, __STRINGIZE__(name)) \ { \ auto get_ptr = &OwnerType::__GETTER_NAME__(name); \ auto set_ptr = &OwnerType::__SETTER_NAME__(name); \ this->getter = (TGetter)(void*)*(void**)(&get_ptr); \ this->setter = (TSetter)(void*)*(void**)(&set_ptr); \ } #define __FIELD_CLASS_CONSTRUCTOR_2_SMART__(type,name) \ template< class OwnerType > \ __CLASS_NAME__(name)(OwnerType * _this, type initvalue) \ : Field<type>(_this, __STRINGIZE__(name), initvalue) \ { \ auto get_ptr = &OwnerType::__GETTER_NAME__(name); \ auto set_ptr = &OwnerType::__SETTER_NAME__(name); \ this->getter = (TGetter)(void*)*(void**)(&get_ptr); \ this->setter = (TSetter)(void*)*(void**)(&set_ptr); \ } #define __FIELD_CLASS_CONSTRUCTOR_1__(type,name) \ template< class OwnerType > \ __CLASS_NAME__(name)(OwnerType * _this) \ : Field<type>(_this, __STRINGIZE__(name)) \ { \ } #define __FIELD_CLASS_CONSTRUCTOR_2__(type,name) \ template< class OwnerType > \ __CLASS_NAME__(name)(OwnerType * _this, type initvalue) \ : Field<type>(_this, __STRINGIZE__(name), initvalue) \ { \ } 


Figures 1 and 2 distinguish constructors with initialization of the field value (2) and without (1). The word SMART indicates the presence of a getter and a setter.

All constructors are also template (the type must be saved and passed to the Field constructor), and they also use the automatic substitution OwnerType. The corresponding Field constructor is called and it is passed to it except this and initialization values ​​(if any) also the field name with the string const char [], obtained by the macro __STRINGIZE__.

Next in the SMART constructors is getting and saving pointers to the getter and setter. It works very strange. The fact is that C ++ strictly refers to casting types of pointers to class methods. This is due to the fact that, given the possibility of inheritance and virtual methods, a pointer to a method may not always be expressed in the same way as a pointer to a function. However, we know that the pointers to our getter and setter can be expressed, for example, by the type void *.

We create temporary variables that will store pointers to methods as given by the C ++ compiler. I wrote the type auto, in fact, it was possible to write explicitly, but it’s more convenient and thanks to C ++ 0x for that.

Next we get pointers to these temporary variables. These pointers are cast to void ** type. Then dereference and get void *. Well, in the end we bring it to TGetter or TSetter types and save.



Finishing touch



Since the field needs the this pointer for normal operation, all fields must be initialized. Therefore, it would be nice to write small macros that allow you to do this conveniently.

 #define initfieldval(name, value) name(this, value) #define initfield(name) name(this) 


The first for initialization value, the second for simple initialization.



That's all!



Using



 #include "basic.h" class Foo : public Basic { public: smartfield(int, i); field(float, f); Foo(); }; Foo::Foo() : initfield(i), initfieldval(f, 3.14) { } int Foo::getterof_i() { printf("Getting field i of class Foo\n"); return i.value; } void Foo::setterof_i(int value) { printf("Setting field i of class Foo\n"); i.value = value; } int main() { Foo foo; int j = foo.i; foo.setField("i", 10); int k = foo.getField("i", -1); float z = foo.f; return 0; } 




Conclusion



So, we got such a tool as class fields with the ability to call by name from a run-time and the ability to specify setters and getters with a fairly simple syntax. I do not claim that this is the best solution to the task, on the contrary, I have ideas on how this could be improved.

Of the minuses, I note the impossibility of creating static fields (for now) and the need to use two different words to initialize fields with and without the default value.



Sources



PS

Everything written here was born solely for the love of C ++.

Of course, I will never write something like this in my work and I do not advise others, because the code is quite difficult to read.



PS2

I am very reluctant to miss the possibility of overloading macros in the preprocessor, even by the number of arguments, and I believe that nothing prevents this.

If it were possible to overload macros by the number of arguments, the field initialization macros looked even more beautiful.

Source: https://habr.com/ru/post/125880/



All Articles