Вступление


Многие языки программирования имеют такой инструмент, как 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.


Комментарии
  • Можно убрать inline, я его добавил потому, что, если компилятор вставит вызовы функции вместо операторов, я достигну своей главной цели — нативности.
  • Почему-то я подозреваю, что property в C# (а то и во всём .NET) и/или Qt так и реализованы, по крайней мере скриптовые языки точно не скупятся на огромное количество указателей под капотом. (UPD: с .NET погорячился, спасибо за поправку в комментариях)

Получение 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'ом...

Комментарии (101)


  1. Videoman
    18.01.2018 15:59

    Не сочтите за наезд, мне действительно хочется раз и навсегда разобраться в данном вопросе. В чем, вообще, преимущество маскировки и неявном вызове функции вместо явного, что все носятся как с «писаной торбой» с этими «свойствами» уже лет 20, если мне не изменяет память? Чем лучше писать object.size вместо object.size()?
    На мой взгляд, «свойства» только сбивают человека читающего код, ведь они создают впечатление обращения к полю класса (всегда быстрая операция), вместо вызова, возможно, сложной логики. Не знаю как в других языках, но в задачах где используется С++ эффективность очень важна. Кстати, по этой же причине многие критикуют перегрузку операторов в С++.


    1. Alozar
      18.01.2018 16:50

      1. Свойства позволяют делать переменные, которые будут readonly вне класса, достаточно сделать приватный сеттер.
      2. На присвоение свойства можно повесить пересчёт параметров а-ля isCorrectValue.
      3. Про с++ не скажу, а вот в c# постоянно использую. Использование свойств позволяет делать связь между данными и интерфейсом в обе стороны. Если вернуться к isCorrectValue. из-за изменения Value определяется значение isCorrectValue, что становится сразу видно в интерфейсе.


      1. Videoman
        18.01.2018 17:35
        +2

        Свойства позволяют делать переменные, которые будут readonly вне класса, достаточно сделать приватный сеттер.
        Ну, т.е., через «свойства» также могут выражаться перечисления и константы. Не вижу ничего хорошего и интуитивного в том, что один и тот же синтаксис выражает столь разные сущности в коде.
        На присвоение свойства можно повесить пересчёт параметров а-ля isCorrectValue.
        Все тоже самое делают и функции, но — явно. Неужели две скобочки () в конце так сильно замедляют ввод кода программистом?


    1. Free_ze
      18.01.2018 17:10

      В чем, вообще, преимущество
      Эстетика.

      «свойства» только сбивают человека читающего код, ведь они создают впечатление обращения к полю класса (всегда быстрая операция), вместо вызова, возможно, сложной логики.… в задачах где используется С++ эффективность очень важна.
      C++ — язык общего назначения, никто не мешает вам писать на нем то, где эффективность не очень важна.
      Польза свойств и прочего сахара в том, что он более интуитивен. Поддержание хорошей читаемости кода — это важная сторона программирования и подобные конструкции помогают улучшить качество жизни среднего программиста. Свойства неплохо себя зарекомендовали в C#, хотя Рихтер и прочие гуру были против них.


      1. Videoman
        18.01.2018 17:18
        +2

        Польза свойств и прочего сахара в том, что он более интуитивен. Поддержание хорошей читаемости кода — это важная сторона программирования и подобные конструкции помогают улучшить качество жизни среднего программиста. Свойства неплохо себя зарекомендовали в C#, хотя Рихтер и прочие гуру были против них.
        По моему, тут вы противоречите себе. Свойства маскируют вызов функции, следовательно они менее интуитивны. Глядя на их синтаксис, в незнакомом коде, вы не сможет понять что это, вызов функции или просто обращение к полю класса. По этому Рихтер и был против.


        1. Alozar
          18.01.2018 17:31

          Глядя на их синтаксис, в незнакомом коде, вы не сможет понять что это, вызов функции или просто обращение к полю класса

          Справедливости ради, вызов get_x() или set_x(value) также не является интуитивным, т.к. вы не знаете, что они делают.
          Чтобы увидеть их внутренности в любом случае придётся лезть в исходник класса, а там уже будет видно, что чем является… если конечно код структурирован, а не разбросан по желанию левой пятки.


          1. Videoman
            18.01.2018 17:38
            +1

            Это везде так, но во всяком случае мы не прикидываемся простым полем. Явное всегда лучше.


            1. Free_ze
              18.01.2018 18:03
              +1

              во всяком случае мы не прикидываемся простым полем.
              Так не прикидывайтесь — выделяйте кейсом, пре-/постфиксами.

              Явное всегда лучше.
              Уместные абстракции все же лучше, чем явная сложность. Тем более, что явность здесь не вносит никакой ясности, зато присутствует синтаксический шум.


              1. Videoman
                18.01.2018 18:12
                +1

                Может вы и правы, но, на мой взгляд, конструкции:
                obj.x — поле
                obj.x — геттер
                obj.x — сеттер
                obj.x — перечисление
                obj.x — константа
                выражают разные сущности, но с помощью одного и того же синтаксиса, и это скорее вредит ясности и пониманию кода, чем помогает.


                1. Alozar
                  18.01.2018 18:24

                  obj.x — поле
                  Что есть поле? Переменная, объект на форме?
                  obj.x — перечисление
                  Это только тип данных на для переменной. Непонятно причём здесь это.
                  obj.x — константа
                  Чем принципиально обращение к константе отличается от обращения к переменной? Синтаксис одинаков.

                  *ZANUDAMODE ON*
                  Вас не возмущает использование * с двумя ПРИНЦИПИАЛЬНО разными значениями?
                  *ZANUDAMODE OFF*


                1. Free_ze
                  18.01.2018 18:25

                  на мой взгляд, конструкции
                  А теперь мой вариант:
                  obj._someField — поле (всегда приватное)
                  obj.SomeProperty — геттер/сеттер
                  FooType::SOME_CONSTANT — константа/значение перечисления (которое тоже константа)

                  Главное — это то, чтобы была возможность через похожий синтаксис выражать похожую семантику.
                  Если программист решил, что пользователю лучше считать метод подобным установке свойства, то зачем мешать ему это делать? Абстракции для того и нужны, чтобы бороться со сложностью.


                  1. Videoman
                    18.01.2018 18:45
                    +2

                    Может быть у вас создается впечатление, что я ярый противник «свойств»,
                    но, на самом деле, мне просто интересно зачем люди столько возятся с этой, на мой взгляд, не удачной концепцией. В С++ «свойств» нет и я считаю это правильно. Я пытаюсь объяснить почему. Иногда излишняя выразительность только запутывает код.

                    Если программист решил, что пользователю лучше считать метод подобным установке свойства, то зачем мешать ему это делать?
                    Всегда старался писать код с точки зрения понятности для читающего, сделать его проще и следовать принципу наименьшего удивления. «Свойства» этому не способствую, на мой взгляд.


                    1. Free_ze
                      18.01.2018 19:02
                      -1

                      Иногда излишняя выразительность только запутывает код.
                      Если читающий плохо понимает то, что читает, то это совсем не выразительный код.

                      Всегда старался писать код с точки зрения понятности для читающего, сделать его проще и следовать принципу наименьшего удивления.
                      Если свойства работают быстро и похожи на присваивание/извлечение значения, то это не удивит пользователя. Это интуитивно понятное поведение, когда ставится знак присваивания.


                    1. klirichek
                      18.01.2018 20:51

                      Не всегда дело в "читающем".
                      Допустим, в многопоточном приложении вдруг кто-то из потоков поменял значение переменной, а потом (спустя пару миллионов итераций) из-за этого всё упало.
                      Если переменная не отличается от свойства — я как раз-таки сделаю логгирование операций в геттерах/сеттерах, и уже в логе попробую разобраться, откуда именно бяка.


            1. Flux
              19.01.2018 06:03

              Не понимаю этого хейта свойств и вечного аргумента про то что свойства «прикидываются обычными полями и изо всех сил хотят обмануть программиста».

              Во первых, если свойства используются в языке где есть конвеншн об их использовании (не в С++) то никакой неоднозначтности и маскировки под поля нет. Когда я вижу выражение

              if (my_object.Length < 10) {
                  my_object.Length = 42;
              }

              я точно знаю что Length можно прочитать и присвоить. Также я знаю что объект останется в консистентном состоянии, что бы я не пытался сотворить использованием оного свойства. Если я знаю что объект сложный то я ожидаю что присвоение свойству вызовет какую-то логику обновления.

              Про маскировку свойств под поля — в том же шарпе когда я пишу my_array.Length я ожидаю что это будет свойство а не поле. А спутать свойство с полем у меня еще ни разу не получалось (даже если я просто смотрел код на гитхабе, про код который я пишу сам и говорить нечего) — комбинация из нейминга, сценария использования и подсказок IDE просто не позволяет ошибиться.

              В реальной жизни объекты обладают именно свойствами которые можно (или нельзя) изменять, а не являются черными ящиками с каким-то состоянием и выведенными наружу способами изменить и получить его. Плюс, код с использованием свойств намного больше похож на математическую запись и не вырождается в лапшу из setProp(expression(getProp())).
              Касательно производительности — подавляющее большинство юзкейсов для свойств это изменение какого-то поля объекта и обновление состояния. Геттер это почти всегда return this.field и инлайнится компилятором, сеттер зачастую немногим тяжелее.
              Вызов же тяжелой логики по изменению свойства либо ожидается (например когда изменение свойства триггерит ивент) либо является лютой ошибкой дизайна, в которой инструмент уже слабо виноват.


              1. Cryvage
                19.01.2018 11:10

                Так дело ещё и в том, что на практике, открытыми полями пользоваться просто не удобно. Даже если забыть про всякие паттерны проектирования, практически в любом месте может понадобится добавить логирование, или событие onChanged. А уж без проверки на корректность присваиваемого значения, в большинстве случаев, вообще не обойтись, что делает объявление сеттера (а следовательно и геттера) практически неизбежным. Как следствие, все поля по умолчанию заменяются геттерами и сеттерами. Даже если изначально внутри простой return и присваивание соответственно. Это делается просто «про запас».
                И что получается в итоге? Как в случае со свойствами «методы маскируются под поля», так в случае с геттерами и сеттерами — «поля маскируются под методы». Со свойствами, или без них, всё сводится к единой форме. В конце концов, это следствие дизайна, а не синтаксиса. Мы хотим иметь возможность менять поведение объекта: добавлять дополнительные проверки и обработчики при доступе к данным, или наоборот, оптимизировать, убирая то, что стало не нужно. И все это мы хотим делать, не ломая при этом существующий пользовательский код. А для этого доступ к полям и вызов методов должен выглядеть одинаково. По другому просто не получается.


              1. Videoman
                19.01.2018 11:27

                Не знаю про Шарп, но раз уж вы затронули С++, то мне кажется что любые конструкции, которые выглядят компактно, но могут под собой иметь тяжелую реализацию не желательны. Знаете почему многие программисты, особенно которым нужно писать быстрый код по тем или иным причинам, до сих пор предпочитают чистый С, не смотря на то что он опасней и там много ручной работы по зачистке ресурсов и т.д.? Потому что в С смотря на код функции мы сразу можем прикинуть сколько она будет выполнятся. Если нам долго писать на С, то и выполнятся это будет долго и наоборот. Объем кода на С пропорционален объему работы которую должен выполнить процессор. В С++ с этим правилом уже все не так однозначно, но при определенных ограничениях вы все еще можете писать как на С, так сказать чувствуя железо. «Свойства» в С++ только мешают этому.


                1. Free_ze
                  19.01.2018 11:43

                  «Свойства» в С++ только мешают этому.

                  Кто мешает не использовать то, что мешает?


                  1. Videoman
                    19.01.2018 12:01

                    Вопрос из серии: кто мешает не делать в коде ошибок?! Если есть возможность ее будут использовать, в том числе и неправильно. Свойства, по моему мнению, лишь увеличивают энтропию, ничего не давая взамен.


                    1. Free_ze
                      19.01.2018 12:06

                      Вопрос из серии: кто мешает не делать в коде ошибок?!
                      Никто не мешает же) Или вы сознательно ошибки допускаете?

                      лишь увеличивают энтропию, ничего не давая взамен.
                      Повышение читаемости кода, что как раз помогает делать меньше ошибок. У брейнфака энтропия минимальна, только классным языком его никто не считает.


                      1. Videoman
                        19.01.2018 12:17

                        Ну так читаемость кода повышается за счет однозначности конструкции, а не за счет наделения одних и тех же выражений разными смыслами в зависимости от контекста, как в случае со «свойствами».

                        У брейнфака энтропия минимальна, только классным языком его никто не считает.
                        Потому-что слишком многословный и низкоуровневый. Под каждую задачу нужен свой уровень абстракции.


                        1. Alozar
                          19.01.2018 12:20
                          +1

                          В таком случае с++ крайне нечитаемый язык
                          1. Константа и переменная пишутся идентично (их различают только по стилю именования;
                          2. Умножение и указатель обозначаются одинаковым символом.


                          1. Videoman
                            19.01.2018 12:26

                            А я и не утверждал что С++ образец для подражания. За это его и критикуют. Но это не повод усугублять проблемы еще сильнее.


                          1. mayorovp
                            19.01.2018 12:28

                            3. Взятие адреса, побитовое «и» и ссылка тоже обозначаются одинаковым символом.
                            4. Параметры шаблона за каким-то фигом обрамляются операторами «меньше» и «больше».
                            5. А уж что с квадратными скобками сотворили…


                            1. Alozar
                              19.01.2018 12:31

                              На правах не очень разбирающегося в с++.
                              Что сделали с квадратными скобками?


                              1. mayorovp
                                19.01.2018 12:34

                                Превратили в часть синтаксиса лямбды, оставив им при этом две формы индексирования…


                                1. Alozar
                                  19.01.2018 12:39

                                  count_if(srcVec.begin(), srcVec.end(), [] (int _n)
                                  		{
                                  			return (_n % 2) == 0;
                                  		});

                                  Если вы про это… мда, это пипец какой-то


                        1. mayorovp
                          19.01.2018 12:26

                          Просто вы привыкли что x.foo — это всегда поле, вот вам свойство и кажется каким-то новым смыслом.


                          В то же время, можно считать x.foo по умолчанию обращением к свойству, а поле считать особым свойством у которого можно еще и взять адрес.


                        1. Free_ze
                          19.01.2018 12:30
                          +1

                          Вы с завидным упорством игнорируете то, что я вам пишу(1, 2): для похожих конструкций должны быть похожие «смыслы», это ответственность программиста. Как и названия функций, имена переменных, надписи на заборах и т.п.

                          Под каждую задачу нужен свой уровень абстракции.
                          В точку! Поэтому в начале этого тредика я и предложил не использовать те конструкции, которые не соответствуют текущему уровню абстракции.

                          ЗЫ Не нужно смотреть на C++ лишь через призму Си, это язык с гораздо более широкими возможностями по написанию высокоуровневого кода.


                          1. Videoman
                            19.01.2018 12:49

                            Вы с завидным упорством игнорируете то, что я вам пишу(1, 2): для похожих конструкций должны быть похожие «смыслы», это ответственность программиста. Как и названия функций, имена переменных, надписи на заборах и т.п.
                            Ничего я не игнорирую. Я понимаю вашу точку зрения. Я понимаю что перегрузка операции "+" в С++ должна реализовывать определенную семантику и т.п., а свойства (в языках где они есть) должны быть такими-то и такими-то. Просто на практике, к сожалению, все эти правила разбиваются о реальность, где есть время разработки, средняя квалификация программиста и т.д.
                            Не нужно смотреть на C++ лишь через призму Си, это язык с гораздо более широкими возможностями по написанию высокоуровневого кода.
                            А я и не смотрю так, я как раз, в основном, программирую на С++ и хорошо представляю все его ограничения и возможности. Но согласитесь, без нормальных модулей и рефлексии, на С++ тяжело писать уж очень высокоуровневый распределенный код. Эти ограничения, на практике, сразу загоняют его в определенную нишу.


                            1. Free_ze
                              19.01.2018 13:03

                              Просто на практике, к сожалению, все эти правила разбиваются о реальность, где есть время разработки, средняя квалификация программиста и т.д.
                              И что же, скажем, с перегрузками? Я встречал стайл-гайды, где перегрузка операторов запрещена. У кого-то запрещены шаблоны. Большинство, как вы говорите, использует Си-совместимое подмножество. Это нормально и вполне регулируется.

                              Но согласитесь, без нормальных модулей и рефлексии, на С++ тяжело писать уж очень высокоуровневый распределенный код.
                              Безусловно соглашусь! Я, как и сотни тысяч разработчиков, жду эти злосчастные модули и рефлекшн в будущем. Но, кмк, не стоит из-за этого отказываться отказываться от других вещей, которые кому-то могут быть полезны (и не навязывают ничего всем остальным). Язык мутирует развивается сейчас как раз в сторону высокоуровневой разработки, пытаясь стать great again! для прикладного софта.


                              1. Videoman
                                19.01.2018 13:24

                                И что же, скажем, с перегрузками? Я встречал стайл-гайды, где перегрузка операторов запрещена. У кого-то запрещены шаблоны. Большинство, как вы говорите, использует Си-совместимое подмножество. Это нормально и вполне регулируется.
                                Ну тут нет однозначного решения. Приходится действовать по обстоятельствам. Большие конторы, где очень разный уровень разработчиков, вводят такие стайл-гайды. В меньших конторах, или отделах, где можно, в среднем, поднять уровень повыше, могут быть послабления, и т.д. Если вы одиночка, то можете использовать что хотите и как хотите.
                                Но, кмк, не стоит из-за этого отказываться отказываться от других вещей, которые кому-то могут быть полезны (и не навязывают ничего всем остальным).
                                Я не призываю отказываться разных парадигм, на то это и С++. Но я против их смешивания и я против конструкций которые дублируют уже существующие, запутывая человека читающего код. Ну не убедили меня, пока, что код:
                                elementCount = arr.count;
                                arr.count = elementCount + 10;
                                понятней или короче чем:
                                elementCount = arr.count();
                                arr.resize(elementCount + 10);
                                особенно если у resize есть перегрузки и дополнительные параметры. Ну ведь все-равно придется делать функции, на практике.
                                Но это все, естественно, мое личное мнение.


                                1. Videoman
                                  19.01.2018 16:47
                                  -1

                                  Точнее даже так — хороший код всегда стремиться к операциям или функциям без побочных эффектов — «чистым» функциям. Семантика «свойства» — это «присвоение/взятие» с побочным эффектом. Вот я — против побочных эффектов, так как знаю что, на практике, такой код тяжело поддерживать. Вот так, мысль, будет точнее.


                                  1. Free_ze
                                    19.01.2018 17:00
                                    +2

                                    Замена свойств на методы-аксессоры не сделает объект иммутабельным. Да и обсуждение шло о синтаксической конструкции, а не приемах построения архитектуры.


                                    1. Videoman
                                      19.01.2018 17:38
                                      +1

                                      Еще раз. Методы объекта могут иметь побочные эффекты и речь не о них. Речь, естественно, об обращении к полям структуры или класса.
                                      item.count = 10;
                                      При взгляде строчку выше, сразу не понятно, это только присвоение или есть еще что-то, что может менять внутреннее состояние класса. Если вы писали этот класс, то это не вызовет у вас проблем, а если вы разбираетесь в чудом коде? Я к тому, что со «свойствами» любое обращение к полям класса может иметь побочку. В С++, и так, слишком много всего может быть переопределено, и за это его также критикуют. «Свойства» же усложнят чтение кода еще сильнее.


                                      1. Free_ze
                                        19.01.2018 18:07

                                        Если вы писали этот класс, то это не вызовет у вас проблем, а если вы разбираетесь в чудом коде?
                                        Представьте, что вместо свойств — методы. Что вы будее делать в случае с методами? Слепо верить или пойдете читать доку? Вот и здесь точно так же.

                                        Свойства не должны изменять состояние объекта «ощущаемое» пользователем. Пример: кэширование, логгирование — хорошо; управление размером массива через записываемое свойство Length — отвратительно.

                                        Но вообще, изменение состояния объекта — это и есть побочный эффект метода.


                                        1. Free_ze
                                          19.01.2018 18:21

                                          Свойства не должны изменять состояние объекта...
                                          … помимо того, что фигурирует в названии


                    1. Alozar
                      19.01.2018 12:13

                      Это вопрос из серии, что не надо использовать топор для забивания гвоздей, если молотком удобнее. Если гвоздь загнётся виновато использование топора или кривой удар?


                      1. Videoman
                        19.01.2018 12:23

                        Если вы любитель, то можно пару гвоздей испортить, ничего страшного, но если ваша профессия — забивать гвозди, то лучше купить автоматический забиватель гвоздей и исключить их загибание, в принципе.


        1. Free_ze
          18.01.2018 17:33
          -1

          Если программист пишет свойство, то он принимает обязательство приблизить его поведение к простому полю. Юзер тоже видит, что это свойство и предполагает, что там будет какая-то логика, но она должна быть достаточно быстрой. Этого негласного соглашения уже достаточно! И мы избавляемся от скобочек и префиксов акссессоров — код становится чище.
          Для наглядности:

          obj.Property = valueProvider.retrieveValue( param );
          
          obj.setProperty( valueProvider.retrieveValue( param ) );

          Кроме того, у нас появляется еще одно средство выразительности: юзер, увидев вызов функции obj.getSomething() поймет, что это наверняка дорогая операция, раз ее не сделали свойством и она выбивается из общего стиля кодовой базы.


          1. Videoman
            18.01.2018 17:44
            +1

            Ах, если бы все писали одноразовые формочки для проектов срок существования которых месяц, возможно, я бы и согласился. Но, на практике, бывают сложные большие проекты, поддерживаемые годами, программисты имеют разную квалификацию, приходят и уходят. Если через некоторое время вызов obj.Property станет «тяжелым», вы будете менять все места вызовов с первого варианта на второй? А если сторонний код уже во всю использует ваш компонент? Ваши правила не проверяются компилятором и слишком не формальны что бы дать Юзеру строгие гарантии, все-равно придется лезть внутрь и проверять.


            1. Alozar
              18.01.2018 17:55
              +1

              Что мешает сделать тяжелой setProperty?
              Функция также будет корректной по синтаксису, но не по сути.


              1. Videoman
                18.01.2018 18:03
                +1

                Проблемы в том, что «свойство» маскирует вызов функции. Грубо:
                — если свойств нет, то obj.x — это обращение к полю класса, а obj.x() — это все что угодно
                — если свойства есть, то obj.x — это все что угодно, также как obj.x() — однозначность теряется
                Про суть я не понял. Функция в любом языке — есть функция, может делать что угодно.


                1. Alozar
                  18.01.2018 18:11

                  Геттер и сеттер по своей сути должны быть очень лёгкими. Никакой бизнес-логики кроме самого присвоения значения переменной и пересчёта свойств типа isCorrectValue быть не должно.
                  Если сеттер (или явная функция, роли не играет) распух, значит в него запихали какую-то логику, которая не связана непосредственно с присвоением значений переменным. Такую функцию явно нужно называть по-другому, а не set_x().


                  1. Videoman
                    18.01.2018 18:22
                    +1

                    Ну это все теория, на практике это, запросто, может не выполнятся. Пример: какое-нибудь свойство visible — оно легкое или нет? Вроде кажется что да. А потом перерисовка контрола стала «тяжелой» и вуаля!!! Проблема с реальным кодом в том, что такие, устные, договоренности очень трудно соблюдать. Вы можете возразить что и функция Visible() может быть «тяжелой» — может, но, во всяком случае, я не буду делать никаких предположений насчет ее «легкости».


                    1. Alozar
                      18.01.2018 18:31
                      +1

                      Это всё прикольно, но как явное указание функции и отсутствие предположения о тяжести функции решит проблему, когда прорисовка тяжелая, а контрол нужно скрыть и показать?
                      Вы говорите, что одно плохо по определению, а другое хорошо, но при этом мы видим только следующие проблемы:
                      1. Теоретическая проблема с непониманием кода, которая решается банальным соблюдением стиля и соглашения
                      2. Тяжесть функции. Вряд-ли вы не согласитесь, что это не проблема использования геттера/сеттера вместо явной функции. Тут проблема в самой логике работы программы.


                      1. Videoman
                        18.01.2018 18:54
                        +1

                        Это уже другой вопрос, но вы не будет изначально делать неправильную архитектуру в расчете на устные гарантии. Как вы обойдете это технически, не важно. Может быть создадите отдельный поток, может быть будете кешировать, может быть дополнительно обрезать область перерисовки. Я просто привел для примера, как иллюстрацию, что в сложный проект сложно контролировать, особенно устными договоренностями.


                  1. grumegargler
                    19.01.2018 06:08

                    буквально на днях общался с коллегой, который рассказывал как в его проекте, сеттер генерировал 200-килобайтный sql-запрос, и что это неправильно, в умных книжках пишут, что сеттер/геттер не должен делать ничего лишнего. А я слушаю и думаю, вначале изобрели потенциальную проблему, а потом пишут о том, как её избегать.


                    1. Flux
                      19.01.2018 06:15

                      Практически для каждой фичи каждого языка можно привести пример неправильного использования.
                      И что, классы/функции/перегрузки/указатели/всечтоугодно теперь не нужны? Сперва изобрели потенциальную проблему, а потом пишут о том, как её избегать. Давайте теперь откажемся от всех абстракций выше какого-то уровня, ведь каждую из них можно криво использовать.


                      1. grumegargler
                        19.01.2018 06:41

                        Согласен с тем, что можно неправильно использовать любую возможность. Но в данном случае, одна и также синтаксическая конструкция таит разный по техническому смыслу процесс получения значения. Один разработчик думает о конструкции как о поле объекта, и использует его повсеместно, например, в ограничении циклов (не думая, что на каждой итерации цикла будет что-то высчитываться). Другой программист – в процессе эволюции, делает из него геттер, реализуя некую несущественную логику, и осаживает производительность системы в целом.


                        1. Alozar
                          19.01.2018 10:09

                          Будет правильно, если функция setValue будет генерировать 200-килобайтный sql-запрос? Если функция по сути является сеттером она не должна делать ничего кроме этого.
                          Давайте уже не будем мешать в одну кучу проблему самих свойств и несоблюдение соглашений о написании кода. Если в программе один разработчик пишет в одном стиле, а другой в другом, это не проблема используемых инструментов, а руководства, которое не следит за программой.


                          1. grumegargler
                            19.01.2018 17:54

                            речь не о том, что правильно, а что нет делается в функции. Речь о том, что если это функция — значит это функция, она была такой, есть и будет. Если это слово через точку — то к такому «удобству» нужно прилагать конвенции (свойства с большой буквы давайте писать) и дополнительную смысловую нагрузку.


                            1. Free_ze
                              19.01.2018 18:14

                              к такому «удобству» нужно прилагать конвенции (свойства с большой буквы давайте писать)
                              Можно и без конвенций. И так логично, что публичное — это не поле.

                              … и дополнительную смысловую нагрузку
                              Семантика как раз осталась привычной: присваивание означает установку значения, а появление в выражении без скобок — извлечение значения.


                1. Cryvage
                  18.01.2018 19:20

                  Ну вот использую я int getX() и void setX(int val). Из контекста понятно что это геттер и сеттер. Или же вариант с перегрузкой int X() и void X(int val) — чуть менее понятно, но зато покороче. И в обоих случаях не понятно, «тяжелые» ли они или нет. Чем это лучше свойств?

                  — если свойств нет, то obj.x — это обращение к полю класса, а obj.x() — это все что угодно

                  У меня доступ абсолютно ко всем полям, если и осуществляется, то только через геттеры и сеттеры. Ну или свойства, если в языке они поддерживаются. Предлагаете мне оставлять торчащие наружу поля чтобы кому-то было понятнее дорогая операция или нет? А как мне тогда позднее связывание делать? В «интерфейсах» объявлять поля? Делать для класса с открытыми полями декоратор или адаптер — тоже то ещё удовольствие.
                  Вот и получается, что отсутствие свойств выливается просто в менее удобный синтаксис. Открытыми полями пользоваться все-равно не вариант. Взять хотя бы ваш пример, когда геттер или сеттер были лёгкими, а стали тяжелыми. В случае с открытыми полями это выльется в необходимость переписывания всего пользовательского кода, ведь очевидно, что логика существенно изменилась, иначе с чего бы было утяжеление. И если изначально у нас было поле, то его придется заменить на геттер и сеттер. Если использовать геттер и сеттер изначально, есть возможность изменить логику, сохранив интерфейс. Всё описанное в последнем абзаце верно, и если заменить геттер и сеттер на свойство. Только синтаксис был бы удобнее, плюс появляется некоторая вероятность (не 100%, к сожалению), что удастся заменить поле на свойство, не сломав пользовательский код.


                  1. Videoman
                    19.01.2018 11:10

                    Вы сейчас мне объясняете элементарные вещи: класс, инкапсуляция, внутреннее состояние, и т.д. Все это я понимаю. Я не понимаю зачем зачем перегружать синтаксис. Вот конкретный вопрос: чем .validElementCount лучше и короче .validElementCount()?


                    1. mayorovp
                      19.01.2018 12:12

                      Пока нет сеттера (мутатора) — ничем не лучше. А вот когда свойство становится доступным не только для чтения…


                    1. Cryvage
                      19.01.2018 22:57

                      Допустим я хочу увеличить этот самый validElementCount на единицу. Что лучше:

                      obj.validElementCount++;
                      

                      или
                      obj.validElementCount( obj.validElementCount() + 1 );
                      

                      И подобных моментов в коде встречается немало. Благодаря свойствам, код мог бы неплохо сократиться, стать более лаконичным. И, что немаловажно, сократилось бы количество скобок. Кто-то может и не согласится, но на мой взгляд, в C++ очень большое количество скобочек на квадратный сантиметр. Иногда они реально мешают, особенно когда в конце выражения закрывается штук пять.
                      Это может казаться мелочью. Капризами. Подумаешь, скобки. Я когда-то тоже так думал. Но со временем пришел к выводу, что синтаксическим сахаром не нужно пренебрегать. Чем больше кода ты пишешь, тем более важную роль начинают играть, казалось бы, незначительные вещи. Каждая отдельно взятая «фича» кажется незначительной (зачастую таковой и является), но всё в совокупности формирует тот самый язык, на котором ты пишешь каждый день. Определённая критическая масса синтаксического сахара качественно меняет опыт использования языка в целом.


                      1. Videoman
                        20.01.2018 13:02

                        Ну ведь костыль же! Тут у вас уже и сеттер и геттер в одном лице, т.е. побочных эффектов может быть еще больше.
                        Возьмем теперь ++obj.validElementCount (преинкремент). Не знаю как в других языках, но в С++ этот вариант придется перегружать отдельно, а также всякие "+=", "-=" и т.д.
                        Теперь давайте возьмем функцию посложнее и добавим всего один параметр:
                        obj.setValidElementCount(obj.validElementCount(eType) + 1, eType);
                        Как в таком случае код использующий «свойства» будет выглядеть?


                        1. Cryvage
                          21.01.2018 16:24

                          В том-то и дело, что ничего перегружать не надо. Если для типа, к которому принадлежит validElementCount, а я предположил, что это int, операции ++, += и т.д. — определены, то всё будет работать без каких-либо дополнительных телодвижений. Ведь именно так это работает с полями. А свойства максимально к ним приближены.
                          И причем тут костыль, и побочные эффекты, я, честно говоря, не понял. Это ведь просто пример свойства открытого на чтение и на запись. Таких в коде полно.
                          Что касается примера с двумя параметрами, возможно в этом случае лучше использовать метод а не свойство. Хотя тут напрашивается индексатор по eType. Как-то так:

                          obj.setValidElementCount[eType]++;
                          

                          Конечно, не для любой пары подойдет использование индексатора. Тут важно учитывать контекст, которого в нашем сферическом примере просто нет. Часто бывает так, что значения удобней записывать вместе, одним методом, а читать по отдельности через свойства.
                          Всё же, главное, что надо держать в уме, это то, что свойства предназначены не для замены любых методов. А, прежде всего, для замены геттеров и сеттеров, которыми мы вынуждены оборачивать поля, ради архитектурных соображений, или просто чтобы иметь возможность добавить немного дополнительной логики: проверка корректности значения, логирование и т.д. И если в каком-то конкретном случае свойство не подходит, а напрашивается обычный метод, то и нет смысла пытаться натянуть сову на глобус.


                          1. Videoman
                            21.01.2018 17:31

                            И причем тут костыль, и побочные эффекты, я, честно говоря, не понял. Это ведь просто пример свойства открытого на чтение и на запись. Таких в коде полно.
                            Вы одной операцией "++" делаете следующее:
                            1. Вызываете геттер (который делает что угодно в общем случае)
                            2. Берете значение
                            3. Инкрементируете
                            4. Вызываете сеттер(который делает что угодно в общем случае)
                            Не многовато ли?

                            obj.setValidElementCount[eType]++;
                            Вы действительно считаете что это хорошо читаемый код?! Мне кажется, что чтобы сильнее запутать, нужно еще постараться.


                            1. Cryvage
                              21.01.2018 19:05
                              +1

                              obj.validElementCount( obj.validElementCount() + 1 );
                              

                              Ну так здесь происходит всё то же самое:
                              1. вызывается геттер int validElementCount(); который так же делает что угодно в общем случае.
                              2. так же берется значение
                              3. прибавляется единица
                              4. вызывается сеттер void validElementCount(int value); который так же делает что угодно.
                              То есть оба кода эквивалентны, просто второй записан намного короче. И, что самое главное, семантически он отражает моё намерение — увеличить значение validElementCount на единицу. Во втором случае это понять сложнее. Особенно если эта операция будет частью какого-то большего выражения.
                              Ну и отдельно стоит сказать, что геттер и сеттер не должны делать «всё что угодно в общем случае». Равно как и свойства. Если код написан настолько плохо, что каждый метод приходится вызывать с опаской, то проблема явно не в свойствах.
                              Вы действительно считаете что это хорошо читаемый код?! Мне кажется, что чтобы сильнее запутать, нужно еще постараться

                              Ну так я и написал, что очень сильно зависит от контекста. По тому участку кода, что вы привели:
                              obj.setValidElementCount(obj.validElementCount(eType) + 1, eType);
                              

                              — можно предположить что eType является своего рода индексом для доступа к значениям validElementCount. В этом случае код вполне хорошо читаем, т.к. отражает семантику.


                              1. Videoman
                                21.01.2018 23:12

                                Послушайте, ясно что и там и там тоже самое. Мы же это и обсуждаем. Было бы очень странным, если бы мы сравнивали не эквивалентный код. В данном случае, короткая запись только вредит ясности понимания того, что делает код, на мой взгляд. Краткость не является самоцелью, главное — четкое пониманию, даже, человеком который впервые видит ваш код.

                                obj.setValidElementCount[eType]++; — это самый ужасный эквивалент:

                                obj.setValidElementCount(obj.validElementCount(eType) + 1, eType);
                                что я видел, вот никогда бы не догадался, извините, но ничего не могу с собой поделать.


                                1. mayorovp
                                  22.01.2018 05:40

                                  То есть все сводится к вашему пониманию прекрасного и ужасного…


                                  1. Videoman
                                    22.01.2018 12:00

                                    Ну, залезть к вам в голову я действительно не могу. Каждый пытается донести свою точку зрения до другого. В это раз не получилось, ну извините. Каждый остался при своем мнении. Значит время рассудит.


            1. Free_ze
              18.01.2018 17:58

              если бы все писали одноразовые формочки для проектов срок существования которых месяц
              Я уже приводил в пример C#, на котором пишут вполне себе энтерпрайз, который поддерживается десятками лет.

              через некоторое время вызов obj.Property станет «тяжелым», вы будете менять все места вызовов
              Он не должен становиться тяжелым, иначе это какая-та беда в архитектуре.


            1. pinbraerts Автор
              19.01.2018 09:28

              Последние примеры полностью обратно совместимы с методами. Если в проекте уже были get_x и set_x, то можно спокойно написать `PropertyHandler` в другом файле например, Header_ext.h, подключить его, и единственным изменением в архитектуре будет являться наследование исходного класса от этого. В использовании ничем не отличается: можно пользоваться a.get_x, а можно a.x.


    1. iCpu
      19.01.2018 07:46

      В таком виде смысл только один: замаскировать свою структуру под простой тип без добавления операторов непосредственно в структуру. То есть, если мы захотим заменить int на, ну, не знаю, HPContainer, но не захотим вкопывать кучу операторов в него, чтобы какой нуб случайно не накосячил, — вот сфера применения. Так себе отговорка, если честно, но лучше, чем ничего.

      На самом деле, нормальные properties раскрывают себя при связывании со скриптовыми языками. Ты определяешь набор свойств класса и экспортируешь его тем или иным способом в скриптовый язык, он сам что-то дёргает по индексу или строке, а на стыке происходит магия. И такими универсальными инструментами производится и приведение типов, и проверка величин, и конструирование объектов, и многое многое другое. Если бы можно было ещё и метаинформацию отдавать, получился бы «убийца Qt moc», но пока стоит достать из стола губозакаточную машинку.

      Другое дело, что нормальные properties хранят в себе чуть больше, чем просто смещения: хотя бы собственный тип. И способ обращения, желательно, сделать чуть более универсальным, аля any с подкастами, а это накладные расходы — и весьма немалые.


    1. sand14
      19.01.2018 08:15

      Коллега, попробуйте обратить внимание на реализацию свойств в Kotlin.


      Дело в том, что в Kotlin нет полей (точнее, внутри геттера и сеттера есть видимое только внутри них backing field).
      Таким образом, нет путаницы при обращении:
      someObject.SomeValue
      this.SomeValue


      SomeValue — всегда свойство, а не поле (хотя да, есть еще подобное обращение к элементам enum).
      А "нагруженность" свойства уже зависит от вас — будут ли у них публичные геттер и сеттер, или сеттер будет приватным, или же в геттере/сеттере еще будет дополнительная логика, или же это вообще будет делегированное свойство.


      А при обращении к полю внутри класса, если геттер/сеттер не реализуют дополнительной логики, то компилятор генерирует обращение сразу к backing field.


      1. Videoman
        19.01.2018 11:33

        В таком случае вопросов нет. Но это все языки другого уровня. Вопрос, зачем свойства нужны С++ где данный синтаксис имеет другое значение и который занимает нишу скорее вместе с С, чем с языками более высокого уровня.


    1. retran
      19.01.2018 18:46

      Это нужно для того, чтобы объединить геттер и сеттер в одну семантическую сущность, на которую можно повесить атрибут и единым образом работать через reflection.


  1. tzlom
    18.01.2018 16:02

    Сомнительная попытка, работает чисто на UB, vptr вам всё испортит.
    Прячет реализацию за синтаксисом, &a.x уже не совсем то что можно ожидать.
    Ну и оптимизатору от такого становится больше работы.


    1. pinbraerts Автор
      19.01.2018 08:59

      Можно сделать __property* operator&() = delete; или внутри оператора сделать свой static_assert("address hook is not allowed in properties"), тогда нельзя будет написать &a.x, property ведь подразумевают вызов функций. Если очень хочется, можно переопределить operator&, чтобы он возвращал адрес backing field, но тогда вся прелесть модификаторов доступа пропадёт.


    1. pinbraerts Автор
      19.01.2018 12:05
      -1

      Что касается виртуальных геттера и сеттера — это не "лишние" указатели: захотели их сделать виртуальными, и сами понимаете, что это повлечёт. Ничего ломаться не будет, если виртуальными их определить внутри Complicated, ведь __property их вызывает через get_this. Так что можно будет унаследовать базовый класс от PropertyHandle, определить там virtual int get_x, а затем в другом классе сделать get_x() override и всё будет работать, как от него ожидалось.


      1. tzlom
        19.01.2018 13:58

        get_this работает только потому, что вам повезло
        В стандарте нет чёткой инструкции как указатели на объекты должны быть размещены, поэтому строго говоря get_this это Undefined Behaviour.
        к примеру что будет, если в Complicated появится виртуальный метод — у вас может быть как (*Complicated)[vPtr:4](*PropertyHandler)[a:sizeof(Axis] так и (*Complicated)(*PropertyHandler)[vPtr:4][a:sizeof(Axis]
        и в первом варианте вам будет грустно и печально.
        Если сюда добавить множественное наследование, или унаследовать Complicated от чего-нибудь, то вариантов становится множество.

        Кстати про накладной расход в 1 байт я не понял, пустая структура занимает 0 байт, это в стандарте прописано.


        1. hdfan2
          19.01.2018 14:23

          Кстати про накладной расход в 1 байт я не понял, пустая структура занимает 0 байт, это в стандарте прописано.

          Нет.


        1. mayorovp
          19.01.2018 14:24

          пустая структура занимает 0 байт, это в стандарте прописано

          Только если она является базовым классом. В противном случае минимальный размер — 1 байт, он нужен чтобы указатели на разные структуры были различимы.


        1. tzlom
          19.01.2018 15:22

          Извиняюсь, это я не разобрался, код действительно рабочий и валидный с точки зрения С++.


  1. Profi_GMan
    18.01.2018 23:01

    Годная статья! Эх… По-больше бы статеек про С++! А то все пишут про веб… А про плюсы забывают, грустно…


  1. IGR2014
    19.01.2018 08:41

    Ну вот смотрите. Вы говорите что:

    самый главный её минус в том, что выражения, которые логически значат 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()))). А я хочу, чтобы выражение в коде выглядело так же, как у меня в голове.

    но при этом не брезгуете в своём коды использованием std::endl, который по-факту является так-же функцией. И для неё даже существует собственная перегрузка оператора <<
    class C {
    
    C& operator<< (std::ostream& (*os)(std::ostream&)) {
            // Вывод куда следует
            return *this;
        }
    
    };

    Как видите, тоже не особо эстетично [sarcasm]. За то эффективно.
    И как уже говорилось в комментариях выше — геттер и сеттер по своей сути должны быть максимально лёгкими и маленькими (в идеале однострочными). Если это не так — надо переписывать весь класс с учётом существующих шаблонов проектирования и идеом.


    1. pinbraerts Автор
      19.01.2018 08:48

      Мне не нравятся скобки при вызове в местах, где по логике должно быть математическое выражение. get_x и set_x можно написать какими угодно маленькими, чтобы "подключить" свойства, надо написать перед классом заглушку (можно в отдельном файле, никакой логики кроме имени поля и модификаторов доступа она не несёт, всё помечено inline, так что скорее всего эта заглушка в коде программы даже не будет существовать. Единственное существенное изменение — унаследовать класс от этой самой заглушки.


      1. IGR2014
        20.01.2018 00:53

        Ну в таком случае всегда можно перегрузить операторы или написать отдельный класс для нужных вычислений, который при грамотном проектировании тоже не оставит от себя и следа после всех оптимизаций. Декоратор, например


    1. mayorovp
      19.01.2018 10:46

      Вы правда не видите разницы между формами записи std::cout << foo << std::endl и std::endl(std::cout << foo)?


      Плох не вызов функции сам по себе, а выворот наизнанку выражения.


      1. IGR2014
        20.01.2018 01:11

        Если в плане получаемого результата — GCC 6.3.0 на -O3 тоже не увидел разницы.
        А если вы про визуальную составляющую — мне вызовы функции удобней, чем наследоваться каждый раз от кучи классов для того чтоб «было как в ентих ваших Шарпах да Джавах». endl — это же не банальный перенос строки. Там под капотом ещё как минимум flush() прячется (а в зависимости от реализации не только он).


  1. AlB80
    19.01.2018 09:42

    1.

    c.x = (c.x * c.x) — 2 * (c.x = c.x / (4 + c.x))

    Порядок вычисления операндов оператора не определён. Изменение переменной и её использование в одном выражении.

    2. Писать inline при реализации метода внутри объявления класса не обязательно.


    1. pinbraerts Автор
      19.01.2018 09:44

      1) Я писал, что пример не имеет смысла, он нужен для показательности: в два раза строка увеличилась.
      2) Я писал, что inline можно убирать, просто если его постааить, компилятор с большой вероятностью по цепочке инлайнов заменит `a.x` на `a.get_x()`


      1. mayorovp
        19.01.2018 10:43

        Никак inline ни на что не повлияет. Компилятор либо умеет инлайнить — и тогда инлайнит все до чего дотянется, либо не умеет — и тогда не инлайнит ничего.


  1. mayorovp
    19.01.2018 10:09

    Почему-то я подозреваю, что property в C# (а то и во всём .NET) и/или Qt так и реализованы, по крайней мере скриптовые языки точно не скупятся на огромное количество указателей под капотом.

    Вот тут вы чепуху сказали. Хранение свойства внутри объекта, а указателя на объект — внутри свойства — это как раз черта подобных велосипедов на языке который свойства не поддерживает.


    В любом языке где свойства являются элементом самого языка задача определения этого самого контекста возлагается на транслятор, рантайм этим не занимается.


    В упомянутом вами .NET свойства являются "сахаром" для вызова методов. Видя обращение к свойству, компилятор пишет в IL обращение к методу. То есть в скомпилированном коде никаких свойств не остается, все свойства остаются только в метаданных.


    В скриптовом языке Javascript дескриптор свойства тоже ничего не хранит про объект. Его даже можно "оторвать" и прилепить совершенно другому объекту:


    const a = { foo: 2 };
    const b = { foo: 3, get bar() { return this.foo*this.foo } }
    console.log(b.bar) // 9
    
    Object.defineProperty(a, "baz", Object.getOwnPropertyDescriptor(b, "bar"));
    console.log(a.baz) // 4

    В рантайме при этом код a.baz интерпретатор превращает во что-то вроде a.[[Get]]("baz", a), что в свою очередь трансформируется в a.[[GetOwnProperty]]("baz").[[Get]].[[Call]](a) — то есть метод дескриптора свойства получает свой контекст (this) входным аргументом, ему не нужно его помнить.


    В скриптовом же языке Python ситуация аналогичная, только тут дескриптор свойства хранится не во внутренних структурах рантайма — а в словаре класса. Тут a.baz будет преобразовано, в качестве одного из возможных вариантов, в type(a).__dict__['baz'].__get__(a, type(a)) (это даже приведено в документации). Опять-таки, нет никакой необходимости хранить ссылку на объект в дескрипторе — потому что она будет передана первым же параметром.


    1. mayorovp
      19.01.2018 10:42

      Кстати, в плюсах можно попробовать пойти по тому же пути. Правда, имитировать поле класса не получится — язык не позволяет вмешаться в этот синтаксис, но зато можно "закосить" под индексатор (c[C::x] = (c[C::x] * c[C::x]) - 2 * (c[C::x] = c[C::x] / (4 + c[C::x]))) или поставить лишнюю пару скобочек: c.x() = (c.x() * c.x()) - 2 * (c.x() = c.x() / (4 + c.x()))


      1. mobi
        19.01.2018 10:58

        Обычно именно такой подход («лишняя пара скобочек») и используется, что-то вроде:

        class Complicated {
        private:
            int _x;
        
        public:
            int x() const {
                std::cout << "x getter called" << std::endl;
                return _x;
            }
        
            int /*или void*/ x(const int v) {
                _x = v;
                std::cout << "x setter called" << std::endl;
                return _x;
            }
        };

        Получается элегантно и «посишечному» c.x( (c.x() * c.x()) - 2 * c.x( c.x() / (4 + c.x()) ) ) (учитывая, что приведенный пример использования относится к категории особых извращений, и обычно сеттер возвращает void, чтобы потом не было мучительно больно при разгребании такого кода).


        1. iCpu
          19.01.2018 11:08

          del


      1. pinbraerts Автор
        19.01.2018 11:58

        Что при этом возвращает c[C::x] или c.x()? Ссылку на скрытое поле? Тогда пропадает инкапсуляция.


        1. mayorovp
          19.01.2018 12:10

          Например, временную структуру «ссылка на свойство» с перегруженными операторами приведения типа и присваивания.


          1. pinbraerts Автор
            19.01.2018 15:52
            -1

            Зачем временная структура, если есть поле, которое выполняет всё то же самое?


            1. mayorovp
              19.01.2018 16:02

              Вы сейчас про какое поле спрашиваете? Если про поле-свойство — то затем чтобы не тратить по байту на каждое.


      1. Mingun
        19.01.2018 19:28

        Тут вы ошибаетесь. Еще лет 10 назад все это было реализовано и находится в первых пяти строчках гугла. Вот, например: http://www.codenet.ru/progr/cpp/cpp-properties.php


    1. pinbraerts Автор
      19.01.2018 12:00

      Спасибо за разъяснение, беру свои слова обратно относительно .NET, в скриптовых указатели на объект, конечно, не хранятся, а на функции (в любом их представлении в скриптовом языке) — хранятся. Изменю статью, как только доберусь до компьютера.


      1. mayorovp
        19.01.2018 12:19

        В том же Питоне в объекте хранятся только поля и ссылка на класс. А все методы, свойства и прочее хранятся уже в словаре класса.

        В javascript объекта класса в чистом виде нет — но их успешно заменяют прототипы. Именно где-то в цепочке прототипов объекта обычно находятся все его методы и сложные свойства.

        Не вижу принципиальных отличий от таблицы виртуальных функций. Разве что связывание идет по имени, а не по индексу — да и то можно исправить скрытыми классами (скрытый класс — термин, используемый интерпретатором v8, который используется в хромах и node.js).


    1. sand14
      20.01.2018 15:35

      В упомянутом вами .NET свойства являются "сахаром" для вызова методов.

      Не совсем так. В .NET к геттеру и сеттеру свойства добавляются атрибуты, означающие, что это именно геттер и сеттер свойства.
      Если просто создать методы getSomeValue и setSomeValue, то свойство не появится, и компилятор не скомпилирует код вида x.SomeValue.


      То, что вы описываете, есть скорее в Java — геттеры и сеттеры никак не помечаются, есть только конвенция именования getSomeValue и setSomeValue.
      И для случая Kotlin, в отличие от .NET, скорее можно сказать, что свойства это сахар — наличие и методов getSomeValue и setSomeValue позволяет предположить, что есть свойство SomeValue (хотя, возможно, там тоже чуть сложнее).


      1. mayorovp
        20.01.2018 16:36

        А я что, говорил про что можно просто создать метод?


        Я писал про то, что обращение foo.Bar компилятором преобразуется в foo.get_Bar(), т.е. в вызов метода. Именно это и называется "синтаксический сахар".


        1. vintage
          21.01.2018 04:24

          Это называется lowering — автоматическое понижение высокоуровневых абстракций до низкоуровневых.


  1. sand14
    19.01.2018 12:12

    Чуть выше я написал про свойства в Kotlin (коли он упомянут в статье).
    Хотелось бы дополнительно уточнить некоторые вещи.
    Зачем нужны свойства в принципе — понятно. Это и "ленивая" инициализация, и, как выше уже отметили, возможность логгирования, обработки OnChanged, и т.д., главное, чтобы свойство соответствовало конвенции, что оно реализует "легкую логику."


    Так вот, отчего же идут постоянные дискуссии, а нужны ли свойства.
    На мой взгляд, дело в том, что до сих пор в большинстве языков они были реализованы не до конца.


    Две самые важные недоделки:


    1. Несвязанность на уровне модели backing field и свойства, это две разные сущности, которые разработчик мысленно объединяет и каждый раз вручную пишет их объединение.
      В результате, как минимум, код выглядит неаккуратно (как группировать поле и свойство?), как максимум — потенциальные ошибки (часто внутри класса идет обращение то к полю, то к свойству, и нельзя сказать как правильно — если логика геттера усложнится, то внутри нужно обращаться к полю или свойству? — а уже есть разные обращения, которые были написаны без какой-либо системы).
    2. Необходимость каждый раз вручную реализовывать паттерны для некоторых задач.
      Например, ленивая инициализация, выдача исключения при попытке чтения еще не инициализированного свойства, проверка допустимости присваиваемого значения, обработка OnChanged, и т.д.

    Так вот, именно эти вопросы решены в Kotlin (соответственно, с помощью backing field, видимого только внутри геттера/сеттера, и мощного механизма делегированных свойств) и, насколько я знаю, в таком объеме решены впервые.
    Другими, словами, в именно Kotlin мы впервые имеем в полном объеме поддержку свойств на уровне модели языка.
    В других языках для реализации полноценных свойств, при определении почти каждого свойства, приходится вручную реализовывать одни и те же паттерны.


    Насколько я понимаю, вопрос делегированных частично решался в .NET в механизме DependencyProperties, но реализовано это не в языке и не в платформе в целом, а в подмножестве платформы (WPF). К тому же, чтобы определить в классе Dependency Property, нужно написать дополнительный код, опять же, по определенному паттерну, что отчасти нивелирует то сокращение кода, которое мы получаем с помощью Dependency Property.