Many programming languages have a tool like properties: C #, Python, Kotlin, Ruby, etc. This tool allows you to call some class method when referring to its "field". In standard C ++, they are not there. If you want to know how you can implement them, I ask for cat.
Everyone knows the implementation using the get_x
and set_x
.
class Complicated { private: int x; public: int get_x() { std::cout << "x getter called" << std::endl; return x; } int set_x(int v) { x = v; std::cout << "x setter called" << std::endl; return x; } };
It is the most obvious solution, besides, no “extra” variables are stored in runtime (except for field x
, it is called backing field , not necessarily and not superfluous), its main drawback is that expressions that logically mean cx = (cx * cx) - 2 * (cx = cx / (4 + cx))
(specifically there is little sense in this example), turn into c.set_x((c.get_x() * c.get_x()) - 2 * c.set_x(c.get_x() / (4 + c.get_x())))
. And I want the expression in the code to look the same as in my head.
You can customize the code in any way: add somewhere inline
or change the return type to void
, remove the backing field or one of the methods, eventually assign const
and volatile
- this does not affect the reasoning. The set of function calls for such a simple arithmetic expression looks at least ugly .
In C ++, as in most other languages, you can overload the operators (+, -, *, /,%, ...). But to do this, you need a wrapper object.
class Complicated { public: class __property { private: int val; public: operator int() { // get std::cout << "x getter called" << std::endl; return val; } int operator=(int v) { // set val = v; std::cout << "x setter called" << std::endl; return val; } } x; };
Now cx = (cx * cx) - 2 * (cx = cx / (4 + cx))
looks like a human being. What if we need to have access to other fields Complicated
?
class Complicated { public: Axis a; class __property { public: operator int() { // get std::cout << "x getter called" << std::endl; return a.get_x(); // ??? 'a' __property } int operator=(int v) { // set std::cout << "x setter called" << std::endl; return a.set_x(v); // ??? 'a' __property } } x; };
Since the operators are reloaded inside the Complicated::__property
, this also has the type Complicated::__property const*
. In other words, in the expression cx = 2
object x
knows nothing at all about the object c
. However, if the implementation of the getter and setter does not require anything from Complicated
, this option is quite logical.
__property
anonymous class.__property
class __property
needed to overload operators.The previous example requires access to Complicated
. The same terminology property implies that get_x
and set_x
will be defined as methods Complicated
. And to call a method inside Complicated
, __property
needs to know this from there.
This method is also quite obvious but not the best. Just store pointers to whatever you like: the getter method, the setter method, this external class, and so on. I have seen such implementations and do not understand why people consider them acceptable. The size of a property increases to 32 (64) bits, or even more, and the pointer is obtained for a memory that is very close to this for the property (almost indicates itself, below will be explained why). Here is my minimalistic version, it is very appropriate to use the link instead of the pointer.
class Complicated { private: Axis a; public: int get_x() { std::cout << "x getter called" << std::endl; return a.get_x(); } int set_x(int v) { std::cout << "x setter called" << std::endl; return a.set_x(v); } class __property { private: Complicated& self; public: __property(Complicated& s): self(s) {} inline operator int() { // get return self.get_x(); } inline int operator=(int v) { // set return self.set_x(v); } } x; Complicated(): x { *this } {} };
This approach can be called an improved version of the first: it fully contains Methods (UPD: It and the following approaches are completely backward compatible with the project, which used getters and setters as Complicated
methods). As you can see, the functionality is defined in Complicated
, and __property
has acquired a less abstract form. However, I don’t like this implementation because of its price in runtime and the need to enter property initialization into the constructor.
The x
field should not exist outside the Complicated
object, and if the wrapper class is also anonymous, then each x
almost guaranteed to be in some Complicated
object. This means that it is relatively safe to get this from the outer class by subtracting its indent from the pointer to x
relative to the beginning of Complicated
.
class Complicated { private: Axis a; public: int get_x() { // get std::cout << "x getter called" << std::endl; return a.get_x(); } int set_x(int v) { // set std::cout << "x setter called" << std::endl; return a.set_x(v); } class __property { private: inline Complicated* get_this() { return reinterpret_cast<Complicated*>(reinterpret_cast<char*>(this) - offsetof(Complicated, x)); } public: inline operator int() { return get_this()->get_x(); } inline int operator=(int v) { return get_this()->set_x(v); } } x; };
Here __property
also has an abstract character, therefore it will be possible to generalize it if necessary. The only drawback is the offsetof for complex (non- POD , hence Complicated ) types is not applicable, gcc warns about this (unlike MSVC, which apparently inserts what you need into the offsetof).
Therefore, you have to wrap __property in a simple structure ( PropertyHandler
), to which the offsetof is applicable, and then bring this from PropertyHandler
to this from Complicated
using static_cast (if Complicated
inherits from PropertyHandler
), which correctly calculates all indents.
template<class T> struct PropertyHandler { struct Property { private: inline const T* get_this() const { return static_cast<const T*>( reinterpret_cast<const PropertyHandler*>( reinterpret_cast<const char*>(this) - offsetof(PropertyHandler, x) ) ); } inline T* get_this() { return static_cast<T*>( reinterpret_cast<PropertyHandler*>( reinterpret_cast<char*>(this) - offsetof(PropertyHandler, x) ) ); } public: inline int operator=(int v) { return get_this()->set_x(v); } inline operator int() { return get_this()->get_x(); } } x; }; class Complicated: PropertyHandler<Complicated> { private: Axis a; public: int get_x() { std::cout << "x getter called" << std::endl; return a.get_x(); } int set_x(int v) { std::cout << "x setter called" << std::endl; return a.set_x(v); } };
As you can see, I already had to create a template so that I could execute static_cast , however, to summarize the definition of Property for very convenient use does not work: it is just completely crutch-like with macros (the name property does not lend itself to customization in Complicated).
This implementation without a backing field takes up only one unused byte (excluding alignment)! And it works the same way as implementation with pointers. With the backing field, it will not take a single “extra” byte, what else is needed for happiness?
The main disadvantage of this approach is the source code curve, but I believe that the syntactic sugar that it brings is worth the effort.
size()
in containers in STL can thus turn into size
(specifically in this example it makes sense to take one of the first implementations, and not the last one - the most sophisticated), or the same begin
with end
'om ...UPD: In fact, the price (in one byte) does not depend on the number of property, because you can put them all in the union.
template<class T> struct PropertyHandler { struct PropertyBase { protected: inline const T* get_this() const { return static_cast<const T*>( reinterpret_cast<const PropertyHandler*>( reinterpret_cast<const char*>(this) - offsetof(PropertyHandler, x) ) ); } inline T* get_this() { return static_cast<T*>( reinterpret_cast<PropertyHandler*>( reinterpret_cast<char*>(this) - offsetof(PropertyHandler, x) ) ); } }; union { class __x: PropertyBase { public: inline int operator=(int v) { return get_this()->set_x(v); } inline operator int() { return get_this()->get_x(); } } x; class __y: PropertyBase { public: inline double operator=(double v) { return get_this()->set_y(v); } inline operator double() { return get_this()->get_y(); } } y; }; }; class Complicated: public PropertyHandler<Complicated> { public: int get_x() { std::cout << "x getter called" << std::endl; return 1; } int set_x(int v) { std::cout << "x setter called" << std::endl; return 2 + v; } double get_y() { std::cout << "y getter called" << std::endl; return 3; } double set_y(double v) { std::cout << "y setter called" << std::endl; return 3 + v; } };
Source: https://habr.com/ru/post/346936/
All Articles