Вступление
Многие языки программирования имеют такой инструмент, как properties: C#, Python, Kotlin, Ruby и т.д. Этот инструмент позволяет вызывать какой-то метод класса при обращении к его "полю". В стандартном C++ их нет если хотите узнать, как можно их реализовать, прошу под кат.
Некоторые моменты...
- Я не Bjarne Stroustrup, поэтому могу ошибаться насчёт внутреннего устройства чего-либо, буду рад поправкам в комментариях.
- В этой статье показаны только идеи реализации Property. Для разных ситуаций подходят разные варианты, в конце статьи нет готовой библиотеки или заголовочного файла.
Методы
Всем известна реализация с помощью методов get_x и 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; } };
Она является самым очевидным решением, к тому же в рантайме не хранятся никакие "лишние" переменные (кроме поля x, оно называется backing field, необязательно и не лишнее), самый главный её минус в том, что выражения, которые логически значат c.x = (c.x * c.x) - 2 * (c.x = c.x / (4 + c.x)) (конкретно в данном примере смысла мало), превращаются в c.set_x((c.get_x() * c.get_x()) - 2 * c.set_x(c.get_x() / (4 + c.get_x()))). А я хочу, чтобы выражение в коде выглядело так же, как у меня в голове.
Вы можете как угодно кастомизировать код: добавить где-то inline или поменять возвращаемый тип на void, убрать backing field или один из методов, в конце концов приписать const и volatile, — это не влияет на рассуждения. Множество вызовов функций для такого простого арифметического выражения выглядит по крайней мере некрасиво.
Операторы
В C++, как и в большинстве других языков, можно перегрузить операторы (+, -, *, /, %, ...). Но чтобы это сделать, нужен объект-обёртка.
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; };
Теперь c.x = (c.x * c.x) - 2 * (c.x = c.x / (4 + c.x)) выглядит по-человечески. А вдруг нам требуется иметь доступ к другим полям 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; };
Так как операторы перегружаются внутри Complicated::__property, то и this там имеет тип Complicated::__property const*. Другими словами, в выражении c.x = 2 объекту x вообще ничего не известно о объекте c. Тем не менее, если реализация геттера и сеттера не требует ничего от Complicated, этот вариант вполне логичен.
- Axis — некоторый объект, осуществляющий, например, физику на оси.
- Можно сделать
__propertyанонимным классом. - Если property без backing field, объект x будет занимать один байт, а не 0. Тут достаточно понятно описано, почему. Из-за выравнивания эта цифра может увеличиваться. Так что если вам очень важен каждый байт памяти, вам остаётся использовать только первый вариант: отдельный класс
__propertyнеобходим для перегрузки операторов.
Сохранение this
Предыдущий пример требует доступа к Complicated. Так же сама терминология property подразумевает, что get_x и set_x будут определены как методы Complicated. А чтобы вызвать метод внутри Complicated, __property должен знать this оттуда.
Этот способ тоже достаточно очевидный но не самый лучший. Просто храним указатели на всё, что нравится: метод-геттер, метод-сеттер, this внешнего класса и так далее. Я видел такие реализации и не понимаю, почему люди считают их приемлемыми. Размер property возрастает до 32 (64) битов, а то и больше, причём указатель получается на память, которая очень близко к this у property (почти сам на себя указывает, ниже будет объяснено, почему). Вот мой минималистичный вариант, он весьма уместно использует ссылку вместо указателя.
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 } {} };
Этот подход можно назвать улучшенным вариантом первого: он полностью содержит Методы (UPD: Он и следующие подходы полностью обратно совместимы с проектом, в котором использовались геттеры и сеттеры как методы Complicated). Как видно, функционал определен в Complicated, а __property приобрело более менее абстрактный вид. Тем не менее, эта реализация мне не нравится из-за её цены в рантайме и необходимости вписывать в конструктор инициализацию property.
Получение this
Поле x не должно существовать вне объекта Complicated, а если класс-обёртка будет ещё и анонимным, то каждый x почти гарантированно будет находиться в каком-то объекте Complicated. Значит, можно относительно безопасно получить this из внешнего класса, вычтя из указателя на x его отступ относительно начала 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; };
Тут __property тоже имеет абстрактный характер, следовательно можно будет его обобщить при надобности. Единственный недостаток — offsetof для сложных (не-POD, отсюда и Complicated) типов неприменим, gcc об этом предупреждает (в отличие от MSVC, который, видимо, вставляет в offsetof что нужно).
Поэтому придётся обернуть __property в простую структуру (PropertyHandler), к которой offsetof применим, а потом привести this из PropertyHandler к this из Complicated с помощью static_cast (если Complicated унаследуется от PropertyHandler), который правильно посчитает все отступы.
Конечный вариант
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); } };
Как видно, мне уже пришлось завести шаблон, чтобы можно было выполнить static_cast, однако обобщить определение Property для очень удобного использования не получается: только совсем костыльнообразно с макросами (имя property не поддаётся кастомизации в Complicated).
Такая реализация без backing field занимает всего один неиспользуемый байт (без учёта выравнивания)! А работает так же, как реализация с указателями. С backing field она не займёт ни единого "лишнего" байта, что ещё нужно для счастья?
Главный минус этого подхода — кривой исходный код, но я считаю, что тот синтаксический сахар, который он приносит стоит затраченных на него усилий.
- Богатство C++ позволяет переопределить по-своему другие операторы (присваивания, бинарных операций, и т.д.), поэтому такую property в отдельных случаях имеет смысл реализовывать под себя, ведь какое-то ключевое слово или два амперсанда (не забывайте перегружать операторы для rvalue, если используются большие объекты) в правильном месте способны значительно улучшить скорость программы. Также открываются новые горизонты отладки...
- Можно наслаждаться лучшими модификаторами доступа, чем в C#! Если хорошо подумать и поставить правильные ключевые слова в нужные места, конечно.
- Property могут сделать какие-то api приятнее, например,
size()у контейнеров в STL может таким образом превратиться вsize(конкретно в этом примере имеет смысл брать одну из первых реализаций, а не последнюю — самую навороченную), или те жеbeginсend'ом...
UPD: На самом деле цена (в один байт) не зависит от количества property, потому что можно их все положить в 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; } };
