Вступление
Многие языки программирования имеют такой инструмент, как 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)
tzlom
18.01.2018 16:02Сомнительная попытка, работает чисто на UB, vptr вам всё испортит.
Прячет реализацию за синтаксисом, &a.x уже не совсем то что можно ожидать.
Ну и оптимизатору от такого становится больше работы.pinbraerts Автор
19.01.2018 08:59Можно сделать
__property* operator&() = delete;
или внутри оператора сделать свойstatic_assert("address hook is not allowed in properties")
, тогда нельзя будет написать&a.x
, property ведь подразумевают вызов функций. Если очень хочется, можно переопределить operator&, чтобы он возвращал адрес backing field, но тогда вся прелесть модификаторов доступа пропадёт.
pinbraerts Автор
19.01.2018 12:05-1Что касается виртуальных геттера и сеттера — это не "лишние" указатели: захотели их сделать виртуальными, и сами понимаете, что это повлечёт. Ничего ломаться не будет, если виртуальными их определить внутри
Complicated
, ведь__property
их вызывает черезget_this
. Так что можно будет унаследовать базовый класс отPropertyHandle
, определить тамvirtual int get_x
, а затем в другом классе сделатьget_x() override
и всё будет работать, как от него ожидалось.tzlom
19.01.2018 13:58get_this работает только потому, что вам повезло
В стандарте нет чёткой инструкции как указатели на объекты должны быть размещены, поэтому строго говоря get_this это Undefined Behaviour.
к примеру что будет, если в Complicated появится виртуальный метод — у вас может быть как (*Complicated)[vPtr:4](*PropertyHandler)[a:sizeof(Axis] так и (*Complicated)(*PropertyHandler)[vPtr:4][a:sizeof(Axis]
и в первом варианте вам будет грустно и печально.
Если сюда добавить множественное наследование, или унаследовать Complicated от чего-нибудь, то вариантов становится множество.
Кстати про накладной расход в 1 байт я не понял, пустая структура занимает 0 байт, это в стандарте прописано.mayorovp
19.01.2018 14:24пустая структура занимает 0 байт, это в стандарте прописано
Только если она является базовым классом. В противном случае минимальный размер — 1 байт, он нужен чтобы указатели на разные структуры были различимы.
tzlom
19.01.2018 15:22Извиняюсь, это я не разобрался, код действительно рабочий и валидный с точки зрения С++.
Profi_GMan
18.01.2018 23:01Годная статья! Эх… По-больше бы статеек про С++! А то все пишут про веб… А про плюсы забывают, грустно…
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]. За то эффективно.
И как уже говорилось в комментариях выше — геттер и сеттер по своей сути должны быть максимально лёгкими и маленькими (в идеале однострочными). Если это не так — надо переписывать весь класс с учётом существующих шаблонов проектирования и идеом.pinbraerts Автор
19.01.2018 08:48Мне не нравятся скобки при вызове в местах, где по логике должно быть математическое выражение. get_x и set_x можно написать какими угодно маленькими, чтобы "подключить" свойства, надо написать перед классом заглушку (можно в отдельном файле, никакой логики кроме имени поля и модификаторов доступа она не несёт, всё помечено inline, так что скорее всего эта заглушка в коде программы даже не будет существовать. Единственное существенное изменение — унаследовать класс от этой самой заглушки.
IGR2014
20.01.2018 00:53Ну в таком случае всегда можно перегрузить операторы или написать отдельный класс для нужных вычислений, который при грамотном проектировании тоже не оставит от себя и следа после всех оптимизаций. Декоратор, например
mayorovp
19.01.2018 10:46Вы правда не видите разницы между формами записи
std::cout << foo << std::endl
иstd::endl(std::cout << foo)
?
Плох не вызов функции сам по себе, а выворот наизнанку выражения.
IGR2014
20.01.2018 01:11Если в плане получаемого результата — GCC 6.3.0 на -O3 тоже не увидел разницы.
А если вы про визуальную составляющую — мне вызовы функции удобней, чем наследоваться каждый раз от кучи классов для того чтоб «было как в ентих ваших Шарпах да Джавах». endl — это же не банальный перенос строки. Там под капотом ещё как минимум flush() прячется (а в зависимости от реализации не только он).
AlB80
19.01.2018 09:421.
c.x = (c.x * c.x) — 2 * (c.x = c.x / (4 + c.x))
Порядок вычисления операндов оператора не определён. Изменение переменной и её использование в одном выражении.
2. Писать inline при реализации метода внутри объявления класса не обязательно.pinbraerts Автор
19.01.2018 09:441) Я писал, что пример не имеет смысла, он нужен для показательности: в два раза строка увеличилась.
2) Я писал, что inline можно убирать, просто если его постааить, компилятор с большой вероятностью по цепочке инлайнов заменит `a.x` на `a.get_x()`mayorovp
19.01.2018 10:43Никак inline ни на что не повлияет. Компилятор либо умеет инлайнить — и тогда инлайнит все до чего дотянется, либо не умеет — и тогда не инлайнит ничего.
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))
(это даже приведено в документации). Опять-таки, нет никакой необходимости хранить ссылку на объект в дескрипторе — потому что она будет передана первым же параметром.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()))
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, чтобы потом не было мучительно больно при разгребании такого кода).
pinbraerts Автор
19.01.2018 11:58Что при этом возвращает c[C::x] или c.x()? Ссылку на скрытое поле? Тогда пропадает инкапсуляция.
mayorovp
19.01.2018 12:10Например, временную структуру «ссылка на свойство» с перегруженными операторами приведения типа и присваивания.
pinbraerts Автор
19.01.2018 15:52-1Зачем временная структура, если есть поле, которое выполняет всё то же самое?
mayorovp
19.01.2018 16:02Вы сейчас про какое поле спрашиваете? Если про поле-свойство — то затем чтобы не тратить по байту на каждое.
Mingun
19.01.2018 19:28Тут вы ошибаетесь. Еще лет 10 назад все это было реализовано и находится в первых пяти строчках гугла. Вот, например: http://www.codenet.ru/progr/cpp/cpp-properties.php
pinbraerts Автор
19.01.2018 12:00Спасибо за разъяснение, беру свои слова обратно относительно .NET, в скриптовых указатели на объект, конечно, не хранятся, а на функции (в любом их представлении в скриптовом языке) — хранятся. Изменю статью, как только доберусь до компьютера.
mayorovp
19.01.2018 12:19В том же Питоне в объекте хранятся только поля и ссылка на класс. А все методы, свойства и прочее хранятся уже в словаре класса.
В javascript объекта класса в чистом виде нет — но их успешно заменяют прототипы. Именно где-то в цепочке прототипов объекта обычно находятся все его методы и сложные свойства.
Не вижу принципиальных отличий от таблицы виртуальных функций. Разве что связывание идет по имени, а не по индексу — да и то можно исправить скрытыми классами (скрытый класс — термин, используемый интерпретатором v8, который используется в хромах и node.js).
sand14
20.01.2018 15:35В упомянутом вами .NET свойства являются "сахаром" для вызова методов.
Не совсем так. В .NET к геттеру и сеттеру свойства добавляются атрибуты, означающие, что это именно геттер и сеттер свойства.
Если просто создать методы getSomeValue и setSomeValue, то свойство не появится, и компилятор не скомпилирует код вида x.SomeValue.
То, что вы описываете, есть скорее в Java — геттеры и сеттеры никак не помечаются, есть только конвенция именования getSomeValue и setSomeValue.
И для случая Kotlin, в отличие от .NET, скорее можно сказать, что свойства это сахар — наличие и методов getSomeValue и setSomeValue позволяет предположить, что есть свойство SomeValue (хотя, возможно, там тоже чуть сложнее).mayorovp
20.01.2018 16:36А я что, говорил про что можно просто создать метод?
Я писал про то, что обращение
foo.Bar
компилятором преобразуется вfoo.get_Bar()
, т.е. в вызов метода. Именно это и называется "синтаксический сахар".vintage
21.01.2018 04:24Это называется lowering — автоматическое понижение высокоуровневых абстракций до низкоуровневых.
sand14
19.01.2018 12:12Чуть выше я написал про свойства в Kotlin (коли он упомянут в статье).
Хотелось бы дополнительно уточнить некоторые вещи.
Зачем нужны свойства в принципе — понятно. Это и "ленивая" инициализация, и, как выше уже отметили, возможность логгирования, обработки OnChanged, и т.д., главное, чтобы свойство соответствовало конвенции, что оно реализует "легкую логику."
Так вот, отчего же идут постоянные дискуссии, а нужны ли свойства.
На мой взгляд, дело в том, что до сих пор в большинстве языков они были реализованы не до конца.
Две самые важные недоделки:
- Несвязанность на уровне модели backing field и свойства, это две разные сущности, которые разработчик мысленно объединяет и каждый раз вручную пишет их объединение.
В результате, как минимум, код выглядит неаккуратно (как группировать поле и свойство?), как максимум — потенциальные ошибки (часто внутри класса идет обращение то к полю, то к свойству, и нельзя сказать как правильно — если логика геттера усложнится, то внутри нужно обращаться к полю или свойству? — а уже есть разные обращения, которые были написаны без какой-либо системы). - Необходимость каждый раз вручную реализовывать паттерны для некоторых задач.
Например, ленивая инициализация, выдача исключения при попытке чтения еще не инициализированного свойства, проверка допустимости присваиваемого значения, обработка OnChanged, и т.д.
Так вот, именно эти вопросы решены в Kotlin (соответственно, с помощью backing field, видимого только внутри геттера/сеттера, и мощного механизма делегированных свойств) и, насколько я знаю, в таком объеме решены впервые.
Другими, словами, в именно Kotlin мы впервые имеем в полном объеме поддержку свойств на уровне модели языка.
В других языках для реализации полноценных свойств, при определении почти каждого свойства, приходится вручную реализовывать одни и те же паттерны.
Насколько я понимаю, вопрос делегированных частично решался в .NET в механизме DependencyProperties, но реализовано это не в языке и не в платформе в целом, а в подмножестве платформы (WPF). К тому же, чтобы определить в классе Dependency Property, нужно написать дополнительный код, опять же, по определенному паттерну, что отчасти нивелирует то сокращение кода, которое мы получаем с помощью Dependency Property.
- Несвязанность на уровне модели backing field и свойства, это две разные сущности, которые разработчик мысленно объединяет и каждый раз вручную пишет их объединение.
Videoman
Не сочтите за наезд, мне действительно хочется раз и навсегда разобраться в данном вопросе. В чем, вообще, преимущество маскировки и неявном вызове функции вместо явного, что все носятся как с «писаной торбой» с этими «свойствами» уже лет 20, если мне не изменяет память? Чем лучше писать object.size вместо object.size()?
На мой взгляд, «свойства» только сбивают человека читающего код, ведь они создают впечатление обращения к полю класса (всегда быстрая операция), вместо вызова, возможно, сложной логики. Не знаю как в других языках, но в задачах где используется С++ эффективность очень важна. Кстати, по этой же причине многие критикуют перегрузку операторов в С++.
Alozar
1. Свойства позволяют делать переменные, которые будут readonly вне класса, достаточно сделать приватный сеттер.
2. На присвоение свойства можно повесить пересчёт параметров а-ля isCorrectValue.
3. Про с++ не скажу, а вот в c# постоянно использую. Использование свойств позволяет делать связь между данными и интерфейсом в обе стороны. Если вернуться к isCorrectValue. из-за изменения Value определяется значение isCorrectValue, что становится сразу видно в интерфейсе.
Videoman
Все тоже самое делают и функции, но — явно. Неужели две скобочки () в конце так сильно замедляют ввод кода программистом?
Free_ze
C++ — язык общего назначения, никто не мешает вам писать на нем то, где эффективность не очень важна.
Польза свойств и прочего сахара в том, что он более интуитивен. Поддержание хорошей читаемости кода — это важная сторона программирования и подобные конструкции помогают улучшить качество жизни среднего программиста. Свойства неплохо себя зарекомендовали в C#, хотя Рихтер и прочие гуру были против них.
Videoman
Alozar
Справедливости ради, вызов get_x() или set_x(value) также не является интуитивным, т.к. вы не знаете, что они делают.
Чтобы увидеть их внутренности в любом случае придётся лезть в исходник класса, а там уже будет видно, что чем является… если конечно код структурирован, а не разбросан по желанию левой пятки.
Videoman
Это везде так, но во всяком случае мы не прикидываемся простым полем. Явное всегда лучше.
Free_ze
Уместные абстракции все же лучше, чем явная сложность. Тем более, что явность здесь не вносит никакой ясности, зато присутствует синтаксический шум.
Videoman
Может вы и правы, но, на мой взгляд, конструкции:
obj.x — поле
obj.x — геттер
obj.x — сеттер
obj.x — перечисление
obj.x — константа
выражают разные сущности, но с помощью одного и того же синтаксиса, и это скорее вредит ясности и пониманию кода, чем помогает.
Alozar
Это только тип данных на для переменной. Непонятно причём здесь это.
Чем принципиально обращение к константе отличается от обращения к переменной? Синтаксис одинаков.
*ZANUDAMODE ON*
Вас не возмущает использование * с двумя ПРИНЦИПИАЛЬНО разными значениями?
*ZANUDAMODE OFF*
Free_ze
obj._someField
— поле (всегда приватное)obj.SomeProperty
— геттер/сеттерFooType::SOME_CONSTANT
— константа/значение перечисления (которое тоже константа)Главное — это то, чтобы была возможность через похожий синтаксис выражать похожую семантику.
Если программист решил, что пользователю лучше считать метод подобным установке свойства, то зачем мешать ему это делать? Абстракции для того и нужны, чтобы бороться со сложностью.
Videoman
Может быть у вас создается впечатление, что я ярый противник «свойств»,
Всегда старался писать код с точки зрения понятности для читающего, сделать его проще и следовать принципу наименьшего удивления. «Свойства» этому не способствую, на мой взгляд.но, на самом деле, мне просто интересно зачем люди столько возятся с этой, на мой взгляд, не удачной концепцией. В С++ «свойств» нет и я считаю это правильно. Я пытаюсь объяснить почему. Иногда излишняя выразительность только запутывает код.
Free_ze
Если свойства работают быстро и похожи на присваивание/извлечение значения, то это не удивит пользователя. Это интуитивно понятное поведение, когда ставится знак присваивания.
klirichek
Не всегда дело в "читающем".
Допустим, в многопоточном приложении вдруг кто-то из потоков поменял значение переменной, а потом (спустя пару миллионов итераций) из-за этого всё упало.
Если переменная не отличается от свойства — я как раз-таки сделаю логгирование операций в геттерах/сеттерах, и уже в логе попробую разобраться, откуда именно бяка.
Flux
Не понимаю этого хейта свойств и вечного аргумента про то что свойства «прикидываются обычными полями и изо всех сил хотят обмануть программиста».
Во первых, если свойства используются в языке где есть конвеншн об их использовании (не в С++) то никакой неоднозначтности и маскировки под поля нет. Когда я вижу выражение
я точно знаю что Length можно прочитать и присвоить. Также я знаю что объект останется в консистентном состоянии, что бы я не пытался сотворить использованием оного свойства. Если я знаю что объект сложный то я ожидаю что присвоение свойству вызовет какую-то логику обновления.
Про маскировку свойств под поля — в том же шарпе когда я пишу my_array.Length я ожидаю что это будет свойство а не поле. А спутать свойство с полем у меня еще ни разу не получалось (даже если я просто смотрел код на гитхабе, про код который я пишу сам и говорить нечего) — комбинация из нейминга, сценария использования и подсказок IDE просто не позволяет ошибиться.
В реальной жизни объекты обладают именно свойствами которые можно (или нельзя) изменять, а не являются черными ящиками с каким-то состоянием и выведенными наружу способами изменить и получить его. Плюс, код с использованием свойств намного больше похож на математическую запись и не вырождается в лапшу из setProp(expression(getProp())).
Касательно производительности — подавляющее большинство юзкейсов для свойств это изменение какого-то поля объекта и обновление состояния. Геттер это почти всегда return this.field и инлайнится компилятором, сеттер зачастую немногим тяжелее.
Вызов же тяжелой логики по изменению свойства либо ожидается (например когда изменение свойства триггерит ивент) либо является лютой ошибкой дизайна, в которой инструмент уже слабо виноват.
Cryvage
Так дело ещё и в том, что на практике, открытыми полями пользоваться просто не удобно. Даже если забыть про всякие паттерны проектирования, практически в любом месте может понадобится добавить логирование, или событие onChanged. А уж без проверки на корректность присваиваемого значения, в большинстве случаев, вообще не обойтись, что делает объявление сеттера (а следовательно и геттера) практически неизбежным. Как следствие, все поля по умолчанию заменяются геттерами и сеттерами. Даже если изначально внутри простой return и присваивание соответственно. Это делается просто «про запас».
И что получается в итоге? Как в случае со свойствами «методы маскируются под поля», так в случае с геттерами и сеттерами — «поля маскируются под методы». Со свойствами, или без них, всё сводится к единой форме. В конце концов, это следствие дизайна, а не синтаксиса. Мы хотим иметь возможность менять поведение объекта: добавлять дополнительные проверки и обработчики при доступе к данным, или наоборот, оптимизировать, убирая то, что стало не нужно. И все это мы хотим делать, не ломая при этом существующий пользовательский код. А для этого доступ к полям и вызов методов должен выглядеть одинаково. По другому просто не получается.
Videoman
Не знаю про Шарп, но раз уж вы затронули С++, то мне кажется что любые конструкции, которые выглядят компактно, но могут под собой иметь тяжелую реализацию не желательны. Знаете почему многие программисты, особенно которым нужно писать быстрый код по тем или иным причинам, до сих пор предпочитают чистый С, не смотря на то что он опасней и там много ручной работы по зачистке ресурсов и т.д.? Потому что в С смотря на код функции мы сразу можем прикинуть сколько она будет выполнятся. Если нам долго писать на С, то и выполнятся это будет долго и наоборот. Объем кода на С пропорционален объему работы которую должен выполнить процессор. В С++ с этим правилом уже все не так однозначно, но при определенных ограничениях вы все еще можете писать как на С, так сказать чувствуя железо. «Свойства» в С++ только мешают этому.
Free_ze
Кто мешает не использовать то, что мешает?
Videoman
Вопрос из серии: кто мешает не делать в коде ошибок?! Если есть возможность ее будут использовать, в том числе и неправильно. Свойства, по моему мнению, лишь увеличивают энтропию, ничего не давая взамен.
Free_ze
Повышение читаемости кода, что как раз помогает делать меньше ошибок. У брейнфака энтропия минимальна, только классным языком его никто не считает.
Videoman
Ну так читаемость кода повышается за счет однозначности конструкции, а не за счет наделения одних и тех же выражений разными смыслами в зависимости от контекста, как в случае со «свойствами».
Потому-что слишком многословный и низкоуровневый. Под каждую задачу нужен свой уровень абстракции.Alozar
В таком случае с++ крайне нечитаемый язык
1. Константа и переменная пишутся идентично (их различают только по стилю именования;
2. Умножение и указатель обозначаются одинаковым символом.
Videoman
А я и не утверждал что С++ образец для подражания. За это его и критикуют. Но это не повод усугублять проблемы еще сильнее.
mayorovp
3. Взятие адреса, побитовое «и» и ссылка тоже обозначаются одинаковым символом.
4. Параметры шаблона за каким-то фигом обрамляются операторами «меньше» и «больше».
5. А уж что с квадратными скобками сотворили…
Alozar
На правах не очень разбирающегося в с++.
Что сделали с квадратными скобками?
mayorovp
Превратили в часть синтаксиса лямбды, оставив им при этом две формы индексирования…
Alozar
Если вы про это… мда, это пипец какой-то
mayorovp
Просто вы привыкли что
x.foo
— это всегда поле, вот вам свойство и кажется каким-то новым смыслом.В то же время, можно считать
x.foo
по умолчанию обращением к свойству, а поле считать особым свойством у которого можно еще и взять адрес.Free_ze
Вы с завидным упорством игнорируете то, что я вам пишу(1, 2): для похожих конструкций должны быть похожие «смыслы», это ответственность программиста. Как и названия функций, имена переменных, надписи на заборах и т.п.
В точку! Поэтому в начале этого тредика я и предложил не использовать те конструкции, которые не соответствуют текущему уровню абстракции.ЗЫ Не нужно смотреть на C++ лишь через призму Си, это язык с гораздо более широкими возможностями по написанию высокоуровневого кода.
Videoman
Free_ze
Безусловно соглашусь! Я, как и сотни тысяч разработчиков, жду эти злосчастные модули и рефлекшн в будущем. Но, кмк, не стоит из-за этого отказываться отказываться от других вещей, которые кому-то могут быть полезны (и не навязывают ничего всем остальным). Язык
мутируетразвивается сейчас как раз в сторону высокоуровневой разработки, пытаясь стать great again! для прикладного софта.Videoman
elementCount = arr.count;
понятней или короче чем:arr.count = elementCount + 10;
elementCount = arr.count();
особенно если у resize есть перегрузки и дополнительные параметры. Ну ведь все-равно придется делать функции, на практике.arr.resize(elementCount + 10);
Но это все, естественно, мое личное мнение.
Videoman
Точнее даже так — хороший код всегда стремиться к операциям или функциям без побочных эффектов — «чистым» функциям. Семантика «свойства» — это «присвоение/взятие» с побочным эффектом. Вот я — против побочных эффектов, так как знаю что, на практике, такой код тяжело поддерживать. Вот так, мысль, будет точнее.
Free_ze
Замена свойств на методы-аксессоры не сделает объект иммутабельным. Да и обсуждение шло о синтаксической конструкции, а не приемах построения архитектуры.
Videoman
Еще раз. Методы объекта могут иметь побочные эффекты и речь не о них. Речь, естественно, об обращении к полям структуры или класса.
item.count = 10;
При взгляде строчку выше, сразу не понятно, это только присвоение или есть еще что-то, что может менять внутреннее состояние класса. Если вы писали этот класс, то это не вызовет у вас проблем, а если вы разбираетесь в чудом коде? Я к тому, что со «свойствами» любое обращение к полям класса может иметь побочку. В С++, и так, слишком много всего может быть переопределено, и за это его также критикуют. «Свойства» же усложнят чтение кода еще сильнее.
Free_ze
Свойства не должны изменять состояние объекта «ощущаемое» пользователем. Пример: кэширование, логгирование — хорошо; управление размером массива через записываемое свойство
Length
— отвратительно.Но вообще, изменение состояния объекта — это и есть побочный эффект метода.
Free_ze
Alozar
Это вопрос из серии, что не надо использовать топор для забивания гвоздей, если молотком удобнее. Если гвоздь загнётся виновато использование топора или кривой удар?
Videoman
Если вы любитель, то можно пару гвоздей испортить, ничего страшного, но если ваша профессия — забивать гвозди, то лучше купить автоматический забиватель гвоздей и исключить их загибание, в принципе.
Free_ze
Если программист пишет свойство, то он принимает обязательство приблизить его поведение к простому полю. Юзер тоже видит, что это свойство и предполагает, что там будет какая-то логика, но она должна быть достаточно быстрой. Этого негласного соглашения уже достаточно! И мы избавляемся от скобочек и префиксов акссессоров — код становится чище.
Для наглядности:
Кроме того, у нас появляется еще одно средство выразительности: юзер, увидев вызов функции
obj.getSomething()
поймет, что это наверняка дорогая операция, раз ее не сделали свойством и она выбивается из общего стиля кодовой базы.Videoman
Ах, если бы все писали одноразовые формочки для проектов срок существования которых месяц, возможно, я бы и согласился. Но, на практике, бывают сложные большие проекты, поддерживаемые годами, программисты имеют разную квалификацию, приходят и уходят. Если через некоторое время вызов obj.Property станет «тяжелым», вы будете менять все места вызовов с первого варианта на второй? А если сторонний код уже во всю использует ваш компонент? Ваши правила не проверяются компилятором и слишком не формальны что бы дать Юзеру строгие гарантии, все-равно придется лезть внутрь и проверять.
Alozar
Что мешает сделать тяжелой setProperty?
Функция также будет корректной по синтаксису, но не по сути.
Videoman
Проблемы в том, что «свойство» маскирует вызов функции. Грубо:
— если свойств нет, то obj.x — это обращение к полю класса, а obj.x() — это все что угодно
— если свойства есть, то obj.x — это все что угодно, также как obj.x() — однозначность теряется
Про суть я не понял. Функция в любом языке — есть функция, может делать что угодно.
Alozar
Геттер и сеттер по своей сути должны быть очень лёгкими. Никакой бизнес-логики кроме самого присвоения значения переменной и пересчёта свойств типа isCorrectValue быть не должно.
Если сеттер (или явная функция, роли не играет) распух, значит в него запихали какую-то логику, которая не связана непосредственно с присвоением значений переменным. Такую функцию явно нужно называть по-другому, а не set_x().
Videoman
Ну это все теория, на практике это, запросто, может не выполнятся. Пример: какое-нибудь свойство visible — оно легкое или нет? Вроде кажется что да. А потом перерисовка контрола стала «тяжелой» и вуаля!!! Проблема с реальным кодом в том, что такие, устные, договоренности очень трудно соблюдать. Вы можете возразить что и функция Visible() может быть «тяжелой» — может, но, во всяком случае, я не буду делать никаких предположений насчет ее «легкости».
Alozar
Это всё прикольно, но как явное указание функции и отсутствие предположения о тяжести функции решит проблему, когда прорисовка тяжелая, а контрол нужно скрыть и показать?
Вы говорите, что одно плохо по определению, а другое хорошо, но при этом мы видим только следующие проблемы:
1. Теоретическая проблема с непониманием кода, которая решается банальным соблюдением стиля и соглашения
2. Тяжесть функции. Вряд-ли вы не согласитесь, что это не проблема использования геттера/сеттера вместо явной функции. Тут проблема в самой логике работы программы.
Videoman
Это уже другой вопрос, но вы не будет изначально делать неправильную архитектуру в расчете на устные гарантии. Как вы обойдете это технически, не важно. Может быть создадите отдельный поток, может быть будете кешировать, может быть дополнительно обрезать область перерисовки. Я просто привел для примера, как иллюстрацию, что в сложный проект сложно контролировать, особенно устными договоренностями.
grumegargler
буквально на днях общался с коллегой, который рассказывал как в его проекте, сеттер генерировал 200-килобайтный sql-запрос, и что это неправильно, в умных книжках пишут, что сеттер/геттер не должен делать ничего лишнего. А я слушаю и думаю, вначале изобрели потенциальную проблему, а потом пишут о том, как её избегать.
Flux
Практически для каждой фичи каждого языка можно привести пример неправильного использования.
И что, классы/функции/перегрузки/указатели/всечтоугодно теперь не нужны? Сперва изобрели потенциальную проблему, а потом пишут о том, как её избегать. Давайте теперь откажемся от всех абстракций выше какого-то уровня, ведь каждую из них можно криво использовать.
grumegargler
Согласен с тем, что можно неправильно использовать любую возможность. Но в данном случае, одна и также синтаксическая конструкция таит разный по техническому смыслу процесс получения значения. Один разработчик думает о конструкции как о поле объекта, и использует его повсеместно, например, в ограничении циклов (не думая, что на каждой итерации цикла будет что-то высчитываться). Другой программист – в процессе эволюции, делает из него геттер, реализуя некую несущественную логику, и осаживает производительность системы в целом.
Alozar
Будет правильно, если функция setValue будет генерировать 200-килобайтный sql-запрос? Если функция по сути является сеттером она не должна делать ничего кроме этого.
Давайте уже не будем мешать в одну кучу проблему самих свойств и несоблюдение соглашений о написании кода. Если в программе один разработчик пишет в одном стиле, а другой в другом, это не проблема используемых инструментов, а руководства, которое не следит за программой.
grumegargler
речь не о том, что правильно, а что нет делается в функции. Речь о том, что если это функция — значит это функция, она была такой, есть и будет. Если это слово через точку — то к такому «удобству» нужно прилагать конвенции (свойства с большой буквы давайте писать) и дополнительную смысловую нагрузку.
Free_ze
Семантика как раз осталась привычной: присваивание означает установку значения, а появление в выражении без скобок — извлечение значения.
Cryvage
Ну вот использую я int getX() и void setX(int val). Из контекста понятно что это геттер и сеттер. Или же вариант с перегрузкой int X() и void X(int val) — чуть менее понятно, но зато покороче. И в обоих случаях не понятно, «тяжелые» ли они или нет. Чем это лучше свойств?
У меня доступ абсолютно ко всем полям, если и осуществляется, то только через геттеры и сеттеры. Ну или свойства, если в языке они поддерживаются. Предлагаете мне оставлять торчащие наружу поля чтобы кому-то было понятнее дорогая операция или нет? А как мне тогда позднее связывание делать? В «интерфейсах» объявлять поля? Делать для класса с открытыми полями декоратор или адаптер — тоже то ещё удовольствие.
Вот и получается, что отсутствие свойств выливается просто в менее удобный синтаксис. Открытыми полями пользоваться все-равно не вариант. Взять хотя бы ваш пример, когда геттер или сеттер были лёгкими, а стали тяжелыми. В случае с открытыми полями это выльется в необходимость переписывания всего пользовательского кода, ведь очевидно, что логика существенно изменилась, иначе с чего бы было утяжеление. И если изначально у нас было поле, то его придется заменить на геттер и сеттер. Если использовать геттер и сеттер изначально, есть возможность изменить логику, сохранив интерфейс. Всё описанное в последнем абзаце верно, и если заменить геттер и сеттер на свойство. Только синтаксис был бы удобнее, плюс появляется некоторая вероятность (не 100%, к сожалению), что удастся заменить поле на свойство, не сломав пользовательский код.
Videoman
Вы сейчас мне объясняете элементарные вещи: класс, инкапсуляция, внутреннее состояние, и т.д. Все это я понимаю. Я не понимаю зачем зачем перегружать синтаксис. Вот конкретный вопрос: чем .validElementCount лучше и короче .validElementCount()?
mayorovp
Пока нет сеттера (мутатора) — ничем не лучше. А вот когда свойство становится доступным не только для чтения…
Cryvage
Допустим я хочу увеличить этот самый validElementCount на единицу. Что лучше:
или
И подобных моментов в коде встречается немало. Благодаря свойствам, код мог бы неплохо сократиться, стать более лаконичным. И, что немаловажно, сократилось бы количество скобок. Кто-то может и не согласится, но на мой взгляд, в C++ очень большое количество скобочек на квадратный сантиметр. Иногда они реально мешают, особенно когда в конце выражения закрывается штук пять.
Это может казаться мелочью. Капризами. Подумаешь, скобки. Я когда-то тоже так думал. Но со временем пришел к выводу, что синтаксическим сахаром не нужно пренебрегать. Чем больше кода ты пишешь, тем более важную роль начинают играть, казалось бы, незначительные вещи. Каждая отдельно взятая «фича» кажется незначительной (зачастую таковой и является), но всё в совокупности формирует тот самый язык, на котором ты пишешь каждый день. Определённая критическая масса синтаксического сахара качественно меняет опыт использования языка в целом.
Videoman
Ну ведь костыль же! Тут у вас уже и сеттер и геттер в одном лице, т.е. побочных эффектов может быть еще больше.
Возьмем теперь
++obj.validElementCount
(преинкремент). Не знаю как в других языках, но в С++ этот вариант придется перегружать отдельно, а также всякие "+=", "-=" и т.д.Теперь давайте возьмем функцию посложнее и добавим всего один параметр:
obj.setValidElementCount(obj.validElementCount(eType) + 1, eType);
Как в таком случае код использующий «свойства» будет выглядеть?
Cryvage
В том-то и дело, что ничего перегружать не надо. Если для типа, к которому принадлежит validElementCount, а я предположил, что это int, операции ++, += и т.д. — определены, то всё будет работать без каких-либо дополнительных телодвижений. Ведь именно так это работает с полями. А свойства максимально к ним приближены.
И причем тут костыль, и побочные эффекты, я, честно говоря, не понял. Это ведь просто пример свойства открытого на чтение и на запись. Таких в коде полно.
Что касается примера с двумя параметрами, возможно в этом случае лучше использовать метод а не свойство. Хотя тут напрашивается индексатор по eType. Как-то так:
Конечно, не для любой пары подойдет использование индексатора. Тут важно учитывать контекст, которого в нашем сферическом примере просто нет. Часто бывает так, что значения удобней записывать вместе, одним методом, а читать по отдельности через свойства.
Всё же, главное, что надо держать в уме, это то, что свойства предназначены не для замены любых методов. А, прежде всего, для замены геттеров и сеттеров, которыми мы вынуждены оборачивать поля, ради архитектурных соображений, или просто чтобы иметь возможность добавить немного дополнительной логики: проверка корректности значения, логирование и т.д. И если в каком-то конкретном случае свойство не подходит, а напрашивается обычный метод, то и нет смысла пытаться натянуть сову на глобус.
Videoman
1. Вызываете геттер (который делает что угодно в общем случае)
2. Берете значение
3. Инкрементируете
4. Вызываете сеттер(который делает что угодно в общем случае)
Не многовато ли?
Вы действительно считаете что это хорошо читаемый код?! Мне кажется, что чтобы сильнее запутать, нужно еще постараться.
Cryvage
Ну так здесь происходит всё то же самое:
1. вызывается геттер int validElementCount(); который так же делает что угодно в общем случае.
2. так же берется значение
3. прибавляется единица
4. вызывается сеттер void validElementCount(int value); который так же делает что угодно.
То есть оба кода эквивалентны, просто второй записан намного короче. И, что самое главное, семантически он отражает моё намерение — увеличить значение validElementCount на единицу. Во втором случае это понять сложнее. Особенно если эта операция будет частью какого-то большего выражения.
Ну и отдельно стоит сказать, что геттер и сеттер не должны делать «всё что угодно в общем случае». Равно как и свойства. Если код написан настолько плохо, что каждый метод приходится вызывать с опаской, то проблема явно не в свойствах.
Ну так я и написал, что очень сильно зависит от контекста. По тому участку кода, что вы привели:
— можно предположить что eType является своего рода индексом для доступа к значениям validElementCount. В этом случае код вполне хорошо читаем, т.к. отражает семантику.
Videoman
Послушайте, ясно что и там и там тоже самое. Мы же это и обсуждаем. Было бы очень странным, если бы мы сравнивали не эквивалентный код. В данном случае, короткая запись только вредит ясности понимания того, что делает код, на мой взгляд. Краткость не является самоцелью, главное — четкое пониманию, даже, человеком который впервые видит ваш код.
что я видел, вот никогда бы не догадался, извините, но ничего не могу с собой поделать.obj.setValidElementCount[eType]++;
— это самый ужасный эквивалент:mayorovp
То есть все сводится к вашему пониманию прекрасного и ужасного…
Videoman
Ну, залезть к вам в голову я действительно не могу. Каждый пытается донести свою точку зрения до другого. В это раз не получилось, ну извините. Каждый остался при своем мнении. Значит время рассудит.
Free_ze
Он не должен становиться тяжелым, иначе это какая-та беда в архитектуре.
pinbraerts Автор
Последние примеры полностью обратно совместимы с методами. Если в проекте уже были get_x и set_x, то можно спокойно написать `PropertyHandler` в другом файле например, Header_ext.h, подключить его, и единственным изменением в архитектуре будет являться наследование исходного класса от этого. В использовании ничем не отличается: можно пользоваться a.get_x, а можно a.x.
iCpu
В таком виде смысл только один: замаскировать свою структуру под простой тип без добавления операторов непосредственно в структуру. То есть, если мы захотим заменить int на, ну, не знаю, HPContainer, но не захотим вкопывать кучу операторов в него, чтобы какой нуб случайно не накосячил, — вот сфера применения. Так себе отговорка, если честно, но лучше, чем ничего.
На самом деле, нормальные properties раскрывают себя при связывании со скриптовыми языками. Ты определяешь набор свойств класса и экспортируешь его тем или иным способом в скриптовый язык, он сам что-то дёргает по индексу или строке, а на стыке происходит магия. И такими универсальными инструментами производится и приведение типов, и проверка величин, и конструирование объектов, и многое многое другое. Если бы можно было ещё и метаинформацию отдавать, получился бы «убийца
Qtmoc», но пока стоит достать из стола губозакаточную машинку.Другое дело, что нормальные properties хранят в себе чуть больше, чем просто смещения: хотя бы собственный тип. И способ обращения, желательно, сделать чуть более универсальным, аля any с подкастами, а это накладные расходы — и весьма немалые.
sand14
Коллега, попробуйте обратить внимание на реализацию свойств в Kotlin.
Дело в том, что в Kotlin нет полей (точнее, внутри геттера и сеттера есть видимое только внутри них backing field).
Таким образом, нет путаницы при обращении:
someObject.SomeValue
this.SomeValue
SomeValue — всегда свойство, а не поле (хотя да, есть еще подобное обращение к элементам enum).
А "нагруженность" свойства уже зависит от вас — будут ли у них публичные геттер и сеттер, или сеттер будет приватным, или же в геттере/сеттере еще будет дополнительная логика, или же это вообще будет делегированное свойство.
А при обращении к полю внутри класса, если геттер/сеттер не реализуют дополнительной логики, то компилятор генерирует обращение сразу к backing field.
Videoman
В таком случае вопросов нет. Но это все языки другого уровня. Вопрос, зачем свойства нужны С++ где данный синтаксис имеет другое значение и который занимает нишу скорее вместе с С, чем с языками более высокого уровня.
retran
Это нужно для того, чтобы объединить геттер и сеттер в одну семантическую сущность, на которую можно повесить атрибут и единым образом работать через reflection.