Pull to refresh

Как вместить property в один байт?

Reading time 6 min
Views 10K

Вступление


Многие языки программирования имеют такой инструмент, как 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;
    }
};
Tags:
Hubs:
+24
Comments 103
Comments Comments 103

Articles