Комментарии 103
На мой взгляд, «свойства» только сбивают человека читающего код, ведь они создают впечатление обращения к полю класса (всегда быстрая операция), вместо вызова, возможно, сложной логики. Не знаю как в других языках, но в задачах где используется С++ эффективность очень важна. Кстати, по этой же причине многие критикуют перегрузку операторов в С++.
2. На присвоение свойства можно повесить пересчёт параметров а-ля isCorrectValue.
3. Про с++ не скажу, а вот в c# постоянно использую. Использование свойств позволяет делать связь между данными и интерфейсом в обе стороны. Если вернуться к isCorrectValue. из-за изменения Value определяется значение isCorrectValue, что становится сразу видно в интерфейсе.
Свойства позволяют делать переменные, которые будут readonly вне класса, достаточно сделать приватный сеттер.Ну, т.е., через «свойства» также могут выражаться перечисления и константы. Не вижу ничего хорошего и интуитивного в том, что один и тот же синтаксис выражает столь разные сущности в коде.
На присвоение свойства можно повесить пересчёт параметров а-ля isCorrectValue.Все тоже самое делают и функции, но — явно. Неужели две скобочки () в конце так сильно замедляют ввод кода программистом?
В чем, вообще, преимуществоЭстетика.
«свойства» только сбивают человека читающего код, ведь они создают впечатление обращения к полю класса (всегда быстрая операция), вместо вызова, возможно, сложной логики.… в задачах где используется С++ эффективность очень важна.C++ — язык общего назначения, никто не мешает вам писать на нем то, где эффективность не очень важна.
Польза свойств и прочего сахара в том, что он более интуитивен. Поддержание хорошей читаемости кода — это важная сторона программирования и подобные конструкции помогают улучшить качество жизни среднего программиста. Свойства неплохо себя зарекомендовали в C#, хотя Рихтер и прочие гуру были против них.
Польза свойств и прочего сахара в том, что он более интуитивен. Поддержание хорошей читаемости кода — это важная сторона программирования и подобные конструкции помогают улучшить качество жизни среднего программиста. Свойства неплохо себя зарекомендовали в C#, хотя Рихтер и прочие гуру были против них.По моему, тут вы противоречите себе. Свойства маскируют вызов функции, следовательно они менее интуитивны. Глядя на их синтаксис, в незнакомом коде, вы не сможет понять что это, вызов функции или просто обращение к полю класса. По этому Рихтер и был против.
Глядя на их синтаксис, в незнакомом коде, вы не сможет понять что это, вызов функции или просто обращение к полю класса
Справедливости ради, вызов get_x() или set_x(value) также не является интуитивным, т.к. вы не знаете, что они делают.
Чтобы увидеть их внутренности в любом случае придётся лезть в исходник класса, а там уже будет видно, что чем является… если конечно код структурирован, а не разбросан по желанию левой пятки.
во всяком случае мы не прикидываемся простым полем.Так не прикидывайтесь — выделяйте кейсом, пре-/постфиксами.
Явное всегда лучше.Уместные абстракции все же лучше, чем явная сложность. Тем более, что явность здесь не вносит никакой ясности, зато присутствует синтаксический шум.
obj.x — поле
obj.x — геттер
obj.x — сеттер
obj.x — перечисление
obj.x — константа
выражают разные сущности, но с помощью одного и того же синтаксиса, и это скорее вредит ясности и пониманию кода, чем помогает.
obj.x — полеЧто есть поле? Переменная, объект на форме?
obj.x — перечислениеЭто только тип данных на для переменной. Непонятно причём здесь это.
obj.x — константаЧем принципиально обращение к константе отличается от обращения к переменной? Синтаксис одинаков.
*ZANUDAMODE ON*
Вас не возмущает использование * с двумя ПРИНЦИПИАЛЬНО разными значениями?
*ZANUDAMODE OFF*
на мой взгляд, конструкцииА теперь мой вариант:
obj._someField
— поле (всегда приватное)obj.SomeProperty
— геттер/сеттерFooType::SOME_CONSTANT
— константа/значение перечисления (которое тоже константа)Главное — это то, чтобы была возможность через похожий синтаксис выражать похожую семантику.
Если программист решил, что пользователю лучше считать метод подобным установке свойства, то зачем мешать ему это делать? Абстракции для того и нужны, чтобы бороться со сложностью.
но, на самом деле, мне просто интересно зачем люди столько возятся с этой, на мой взгляд, не удачной концепцией. В С++ «свойств» нет и я считаю это правильно. Я пытаюсь объяснить почему. Иногда излишняя выразительность только запутывает код.
Если программист решил, что пользователю лучше считать метод подобным установке свойства, то зачем мешать ему это делать?Всегда старался писать код с точки зрения понятности для читающего, сделать его проще и следовать принципу наименьшего удивления. «Свойства» этому не способствую, на мой взгляд.
Иногда излишняя выразительность только запутывает код.Если читающий плохо понимает то, что читает, то это совсем не выразительный код.
Всегда старался писать код с точки зрения понятности для читающего, сделать его проще и следовать принципу наименьшего удивления.Если свойства работают быстро и похожи на присваивание/извлечение значения, то это не удивит пользователя. Это интуитивно понятное поведение, когда ставится знак присваивания.
Не всегда дело в "читающем".
Допустим, в многопоточном приложении вдруг кто-то из потоков поменял значение переменной, а потом (спустя пару миллионов итераций) из-за этого всё упало.
Если переменная не отличается от свойства — я как раз-таки сделаю логгирование операций в геттерах/сеттерах, и уже в логе попробую разобраться, откуда именно бяка.
Во первых, если свойства используются в языке где есть конвеншн об их использовании (не в С++) то никакой неоднозначтности и маскировки под поля нет. Когда я вижу выражение
if (my_object.Length < 10) {
my_object.Length = 42;
}
я точно знаю что Length можно прочитать и присвоить. Также я знаю что объект останется в консистентном состоянии, что бы я не пытался сотворить использованием оного свойства. Если я знаю что объект сложный то я ожидаю что присвоение свойству вызовет какую-то логику обновления.
Про маскировку свойств под поля — в том же шарпе когда я пишу my_array.Length я ожидаю что это будет свойство а не поле. А спутать свойство с полем у меня еще ни разу не получалось (даже если я просто смотрел код на гитхабе, про код который я пишу сам и говорить нечего) — комбинация из нейминга, сценария использования и подсказок IDE просто не позволяет ошибиться.
В реальной жизни объекты обладают именно свойствами которые можно (или нельзя) изменять, а не являются черными ящиками с каким-то состоянием и выведенными наружу способами изменить и получить его. Плюс, код с использованием свойств намного больше похож на математическую запись и не вырождается в лапшу из setProp(expression(getProp())).
Касательно производительности — подавляющее большинство юзкейсов для свойств это изменение какого-то поля объекта и обновление состояния. Геттер это почти всегда return this.field и инлайнится компилятором, сеттер зачастую немногим тяжелее.
Вызов же тяжелой логики по изменению свойства либо ожидается (например когда изменение свойства триггерит ивент) либо является лютой ошибкой дизайна, в которой инструмент уже слабо виноват.
И что получается в итоге? Как в случае со свойствами «методы маскируются под поля», так в случае с геттерами и сеттерами — «поля маскируются под методы». Со свойствами, или без них, всё сводится к единой форме. В конце концов, это следствие дизайна, а не синтаксиса. Мы хотим иметь возможность менять поведение объекта: добавлять дополнительные проверки и обработчики при доступе к данным, или наоборот, оптимизировать, убирая то, что стало не нужно. И все это мы хотим делать, не ломая при этом существующий пользовательский код. А для этого доступ к полям и вызов методов должен выглядеть одинаково. По другому просто не получается.
«Свойства» в С++ только мешают этому.
Кто мешает не использовать то, что мешает?
Вопрос из серии: кто мешает не делать в коде ошибок?!Никто не мешает же) Или вы сознательно ошибки допускаете?
лишь увеличивают энтропию, ничего не давая взамен.Повышение читаемости кода, что как раз помогает делать меньше ошибок. У брейнфака энтропия минимальна, только классным языком его никто не считает.
У брейнфака энтропия минимальна, только классным языком его никто не считает.Потому-что слишком многословный и низкоуровневый. Под каждую задачу нужен свой уровень абстракции.
1. Константа и переменная пишутся идентично (их различают только по стилю именования;
2. Умножение и указатель обозначаются одинаковым символом.
4. Параметры шаблона за каким-то фигом обрамляются операторами «меньше» и «больше».
5. А уж что с квадратными скобками сотворили…
Просто вы привыкли что x.foo
— это всегда поле, вот вам свойство и кажется каким-то новым смыслом.
В то же время, можно считать x.foo
по умолчанию обращением к свойству, а поле считать особым свойством у которого можно еще и взять адрес.
Под каждую задачу нужен свой уровень абстракции.В точку! Поэтому в начале этого тредика я и предложил не использовать те конструкции, которые не соответствуют текущему уровню абстракции.
ЗЫ Не нужно смотреть на C++ лишь через призму Си, это язык с гораздо более широкими возможностями по написанию высокоуровневого кода.
Вы с завидным упорством игнорируете то, что я вам пишу(1, 2): для похожих конструкций должны быть похожие «смыслы», это ответственность программиста. Как и названия функций, имена переменных, надписи на заборах и т.п.Ничего я не игнорирую. Я понимаю вашу точку зрения. Я понимаю что перегрузка операции "+" в С++ должна реализовывать определенную семантику и т.п., а свойства (в языках где они есть) должны быть такими-то и такими-то. Просто на практике, к сожалению, все эти правила разбиваются о реальность, где есть время разработки, средняя квалификация программиста и т.д.
Не нужно смотреть на C++ лишь через призму Си, это язык с гораздо более широкими возможностями по написанию высокоуровневого кода.А я и не смотрю так, я как раз, в основном, программирую на С++ и хорошо представляю все его ограничения и возможности. Но согласитесь, без нормальных модулей и рефлексии, на С++ тяжело писать уж очень высокоуровневый распределенный код. Эти ограничения, на практике, сразу загоняют его в определенную нишу.
Просто на практике, к сожалению, все эти правила разбиваются о реальность, где есть время разработки, средняя квалификация программиста и т.д.И что же, скажем, с перегрузками? Я встречал стайл-гайды, где перегрузка операторов запрещена. У кого-то запрещены шаблоны. Большинство, как вы говорите, использует Си-совместимое подмножество. Это нормально и вполне регулируется.
Но согласитесь, без нормальных модулей и рефлексии, на С++ тяжело писать уж очень высокоуровневый распределенный код.Безусловно соглашусь! Я, как и сотни тысяч разработчиков, жду эти злосчастные модули и рефлекшн в будущем. Но, кмк, не стоит из-за этого отказываться отказываться от других вещей, которые кому-то могут быть полезны (и не навязывают ничего всем остальным). Язык
И что же, скажем, с перегрузками? Я встречал стайл-гайды, где перегрузка операторов запрещена. У кого-то запрещены шаблоны. Большинство, как вы говорите, использует Си-совместимое подмножество. Это нормально и вполне регулируется.Ну тут нет однозначного решения. Приходится действовать по обстоятельствам. Большие конторы, где очень разный уровень разработчиков, вводят такие стайл-гайды. В меньших конторах, или отделах, где можно, в среднем, поднять уровень повыше, могут быть послабления, и т.д. Если вы одиночка, то можете использовать что хотите и как хотите.
Но, кмк, не стоит из-за этого отказываться отказываться от других вещей, которые кому-то могут быть полезны (и не навязывают ничего всем остальным).Я не призываю отказываться разных парадигм, на то это и С++. Но я против их смешивания и я против конструкций которые дублируют уже существующие, запутывая человека читающего код. Ну не убедили меня, пока, что код:
elementCount = arr.count;
arr.count = elementCount + 10;
понятней или короче чем:elementCount = arr.count();
arr.resize(elementCount + 10);
особенно если у resize есть перегрузки и дополнительные параметры. Ну ведь все-равно придется делать функции, на практике.Но это все, естественно, мое личное мнение.
item.count = 10;
При взгляде строчку выше, сразу не понятно, это только присвоение или есть еще что-то, что может менять внутреннее состояние класса. Если вы писали этот класс, то это не вызовет у вас проблем, а если вы разбираетесь в чудом коде? Я к тому, что со «свойствами» любое обращение к полям класса может иметь побочку. В С++, и так, слишком много всего может быть переопределено, и за это его также критикуют. «Свойства» же усложнят чтение кода еще сильнее.
Если вы писали этот класс, то это не вызовет у вас проблем, а если вы разбираетесь в чудом коде?Представьте, что вместо свойств — методы. Что вы будее делать в случае с методами? Слепо верить или пойдете читать доку? Вот и здесь точно так же.
Свойства не должны изменять состояние объекта «ощущаемое» пользователем. Пример: кэширование, логгирование — хорошо; управление размером массива через записываемое свойство
Length
— отвратительно.Но вообще, изменение состояния объекта — это и есть побочный эффект метода.
Для наглядности:
obj.Property = valueProvider.retrieveValue( param );
obj.setProperty( valueProvider.retrieveValue( param ) );
Кроме того, у нас появляется еще одно средство выразительности: юзер, увидев вызов функции
obj.getSomething()
поймет, что это наверняка дорогая операция, раз ее не сделали свойством и она выбивается из общего стиля кодовой базы.Функция также будет корректной по синтаксису, но не по сути.
— если свойств нет, то obj.x — это обращение к полю класса, а obj.x() — это все что угодно
— если свойства есть, то obj.x — это все что угодно, также как obj.x() — однозначность теряется
Про суть я не понял. Функция в любом языке — есть функция, может делать что угодно.
Если сеттер (или явная функция, роли не играет) распух, значит в него запихали какую-то логику, которая не связана непосредственно с присвоением значений переменным. Такую функцию явно нужно называть по-другому, а не set_x().
Вы говорите, что одно плохо по определению, а другое хорошо, но при этом мы видим только следующие проблемы:
1. Теоретическая проблема с непониманием кода, которая решается банальным соблюдением стиля и соглашения
2. Тяжесть функции. Вряд-ли вы не согласитесь, что это не проблема использования геттера/сеттера вместо явной функции. Тут проблема в самой логике работы программы.
И что, классы/функции/перегрузки/указатели/всечтоугодно теперь не нужны? Сперва изобрели потенциальную проблему, а потом пишут о том, как её избегать. Давайте теперь откажемся от всех абстракций выше какого-то уровня, ведь каждую из них можно криво использовать.
Давайте уже не будем мешать в одну кучу проблему самих свойств и несоблюдение соглашений о написании кода. Если в программе один разработчик пишет в одном стиле, а другой в другом, это не проблема используемых инструментов, а руководства, которое не следит за программой.
к такому «удобству» нужно прилагать конвенции (свойства с большой буквы давайте писать)Можно и без конвенций. И так логично, что публичное — это не поле.
… и дополнительную смысловую нагрузкуСемантика как раз осталась привычной: присваивание означает установку значения, а появление в выражении без скобок — извлечение значения.
— если свойств нет, то obj.x — это обращение к полю класса, а obj.x() — это все что угодно
У меня доступ абсолютно ко всем полям, если и осуществляется, то только через геттеры и сеттеры. Ну или свойства, если в языке они поддерживаются. Предлагаете мне оставлять торчащие наружу поля чтобы кому-то было понятнее дорогая операция или нет? А как мне тогда позднее связывание делать? В «интерфейсах» объявлять поля? Делать для класса с открытыми полями декоратор или адаптер — тоже то ещё удовольствие.
Вот и получается, что отсутствие свойств выливается просто в менее удобный синтаксис. Открытыми полями пользоваться все-равно не вариант. Взять хотя бы ваш пример, когда геттер или сеттер были лёгкими, а стали тяжелыми. В случае с открытыми полями это выльется в необходимость переписывания всего пользовательского кода, ведь очевидно, что логика существенно изменилась, иначе с чего бы было утяжеление. И если изначально у нас было поле, то его придется заменить на геттер и сеттер. Если использовать геттер и сеттер изначально, есть возможность изменить логику, сохранив интерфейс. Всё описанное в последнем абзаце верно, и если заменить геттер и сеттер на свойство. Только синтаксис был бы удобнее, плюс появляется некоторая вероятность (не 100%, к сожалению), что удастся заменить поле на свойство, не сломав пользовательский код.
obj.validElementCount++;
или
obj.validElementCount( obj.validElementCount() + 1 );
И подобных моментов в коде встречается немало. Благодаря свойствам, код мог бы неплохо сократиться, стать более лаконичным. И, что немаловажно, сократилось бы количество скобок. Кто-то может и не согласится, но на мой взгляд, в C++ очень большое количество скобочек на квадратный сантиметр. Иногда они реально мешают, особенно когда в конце выражения закрывается штук пять.
Это может казаться мелочью. Капризами. Подумаешь, скобки. Я когда-то тоже так думал. Но со временем пришел к выводу, что синтаксическим сахаром не нужно пренебрегать. Чем больше кода ты пишешь, тем более важную роль начинают играть, казалось бы, незначительные вещи. Каждая отдельно взятая «фича» кажется незначительной (зачастую таковой и является), но всё в совокупности формирует тот самый язык, на котором ты пишешь каждый день. Определённая критическая масса синтаксического сахара качественно меняет опыт использования языка в целом.
Возьмем теперь
++obj.validElementCount
(преинкремент). Не знаю как в других языках, но в С++ этот вариант придется перегружать отдельно, а также всякие "+=", "-=" и т.д.Теперь давайте возьмем функцию посложнее и добавим всего один параметр:
obj.setValidElementCount(obj.validElementCount(eType) + 1, eType);
Как в таком случае код использующий «свойства» будет выглядеть?
И причем тут костыль, и побочные эффекты, я, честно говоря, не понял. Это ведь просто пример свойства открытого на чтение и на запись. Таких в коде полно.
Что касается примера с двумя параметрами, возможно в этом случае лучше использовать метод а не свойство. Хотя тут напрашивается индексатор по eType. Как-то так:
obj.setValidElementCount[eType]++;
Конечно, не для любой пары подойдет использование индексатора. Тут важно учитывать контекст, которого в нашем сферическом примере просто нет. Часто бывает так, что значения удобней записывать вместе, одним методом, а читать по отдельности через свойства.
Всё же, главное, что надо держать в уме, это то, что свойства предназначены не для замены любых методов. А, прежде всего, для замены геттеров и сеттеров, которыми мы вынуждены оборачивать поля, ради архитектурных соображений, или просто чтобы иметь возможность добавить немного дополнительной логики: проверка корректности значения, логирование и т.д. И если в каком-то конкретном случае свойство не подходит, а напрашивается обычный метод, то и нет смысла пытаться натянуть сову на глобус.
И причем тут костыль, и побочные эффекты, я, честно говоря, не понял. Это ведь просто пример свойства открытого на чтение и на запись. Таких в коде полно.Вы одной операцией "++" делаете следующее:
1. Вызываете геттер (который делает что угодно в общем случае)
2. Берете значение
3. Инкрементируете
4. Вызываете сеттер(который делает что угодно в общем случае)
Не многовато ли?
obj.setValidElementCount[eType]++;
Вы действительно считаете что это хорошо читаемый код?! Мне кажется, что чтобы сильнее запутать, нужно еще постараться.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. В этом случае код вполне хорошо читаем, т.к. отражает семантику.
obj.setValidElementCount[eType]++;
— это самый ужасный эквивалент:obj.setValidElementCount(obj.validElementCount(eType) + 1, eType);
что я видел, вот никогда бы не догадался, извините, но ничего не могу с собой поделать.если бы все писали одноразовые формочки для проектов срок существования которых месяцЯ уже приводил в пример C#, на котором пишут вполне себе энтерпрайз, который поддерживается десятками лет.
через некоторое время вызов obj.Property станет «тяжелым», вы будете менять все места вызововОн не должен становиться тяжелым, иначе это какая-та беда в архитектуре.
На самом деле, нормальные properties раскрывают себя при связывании со скриптовыми языками. Ты определяешь набор свойств класса и экспортируешь его тем или иным способом в скриптовый язык, он сам что-то дёргает по индексу или строке, а на стыке происходит магия. И такими универсальными инструментами производится и приведение типов, и проверка величин, и конструирование объектов, и многое многое другое. Если бы можно было ещё и метаинформацию отдавать, получился бы «убийца
Другое дело, что нормальные properties хранят в себе чуть больше, чем просто смещения: хотя бы собственный тип. И способ обращения, желательно, сделать чуть более универсальным, аля any с подкастами, а это накладные расходы — и весьма немалые.
Коллега, попробуйте обратить внимание на реализацию свойств в Kotlin.
Дело в том, что в Kotlin нет полей (точнее, внутри геттера и сеттера есть видимое только внутри них backing field).
Таким образом, нет путаницы при обращении:
someObject.SomeValue
this.SomeValue
SomeValue — всегда свойство, а не поле (хотя да, есть еще подобное обращение к элементам enum).
А "нагруженность" свойства уже зависит от вас — будут ли у них публичные геттер и сеттер, или сеттер будет приватным, или же в геттере/сеттере еще будет дополнительная логика, или же это вообще будет делегированное свойство.
А при обращении к полю внутри класса, если геттер/сеттер не реализуют дополнительной логики, то компилятор генерирует обращение сразу к 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())))
Кроме того, геттеры и сеттеры по сути — обычные методы, возвращающие или устанавливающие значение некоторой единицы данных. Но при этом, они смешиваются с другими методами, которые могут реализовывать сложную логику, операцию манипулирующую несколькими единицами данных, и возвращать, например, код статуса этой операции. Обычно, геттеры и сеттеры визуально отличаются наличием префиксов get и set, но нередко бывают исключения, например — STL, где широко используются имена геттеров вроде begin(), end(), или size(). Конечно, каждый кто знает С++, знает и такие особенности STL, но столкнувшись с подобными именами методов в незнакомой библиотеке, их легко принять за имена операций а не геттеров.
Свойства же вряд ли можно спутать с чем-то кроме свойств: поля классов обычно недоступны извне, и кроме того, их имена обычно начинаются с префикса "_". Исходя из этого, я с трудом могу себе представить, как вы будете напрямую обращается к полям класса ради оптимизации. Более того, для написания быстрого и эффективного кода вам в любом случае придётся досконально изучить внутреннюю логику класса, после чего вопросы типа «я обращаюсь к полю или к свойству?» отпадут сами собой.
Прячет реализацию за синтаксисом, &a.x уже не совсем то что можно ожидать.
Ну и оптимизатору от такого становится больше работы.
Можно сделать __property* operator&() = delete;
или внутри оператора сделать свой static_assert("address hook is not allowed in properties")
, тогда нельзя будет написать &a.x
, property ведь подразумевают вызов функций. Если очень хочется, можно переопределить operator&, чтобы он возвращал адрес backing field, но тогда вся прелесть модификаторов доступа пропадёт.
Что касается виртуальных геттера и сеттера — это не "лишние" указатели: захотели их сделать виртуальными, и сами понимаете, что это повлечёт. Ничего ломаться не будет, если виртуальными их определить внутри Complicated
, ведь __property
их вызывает через get_this
. Так что можно будет унаследовать базовый класс от PropertyHandle
, определить там virtual int get_x
, а затем в другом классе сделать get_x() override
и всё будет работать, как от него ожидалось.
В стандарте нет чёткой инструкции как указатели на объекты должны быть размещены, поэтому строго говоря get_this это Undefined Behaviour.
к примеру что будет, если в Complicated появится виртуальный метод — у вас может быть как (*Complicated)[vPtr:4](*PropertyHandler)[a:sizeof(Axis] так и (*Complicated)(*PropertyHandler)[vPtr:4][a:sizeof(Axis]
и в первом варианте вам будет грустно и печально.
Если сюда добавить множественное наследование, или унаследовать Complicated от чего-нибудь, то вариантов становится множество.
Кстати про накладной расход в 1 байт я не понял, пустая структура занимает 0 байт, это в стандарте прописано.
самый главный её минус в том, что выражения, которые логически значат 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]. За то эффективно.
И как уже говорилось в комментариях выше — геттер и сеттер по своей сути должны быть максимально лёгкими и маленькими (в идеале однострочными). Если это не так — надо переписывать весь класс с учётом существующих шаблонов проектирования и идеом.
Мне не нравятся скобки при вызове в местах, где по логике должно быть математическое выражение. get_x и set_x можно написать какими угодно маленькими, чтобы "подключить" свойства, надо написать перед классом заглушку (можно в отдельном файле, никакой логики кроме имени поля и модификаторов доступа она не несёт, всё помечено inline, так что скорее всего эта заглушка в коде программы даже не будет существовать. Единственное существенное изменение — унаследовать класс от этой самой заглушки.
Вы правда не видите разницы между формами записи std::cout << foo << std::endl
и std::endl(std::cout << foo)
?
Плох не вызов функции сам по себе, а выворот наизнанку выражения.
А если вы про визуальную составляющую — мне вызовы функции удобней, чем наследоваться каждый раз от кучи классов для того чтоб «было как в ентих ваших Шарпах да Джавах». endl — это же не банальный перенос строки. Там под капотом ещё как минимум flush() прячется (а в зависимости от реализации не только он).
c.x = (c.x * c.x) — 2 * (c.x = c.x / (4 + c.x))
Порядок вычисления операндов оператора не определён. Изменение переменной и её использование в одном выражении.
2. Писать inline при реализации метода внутри объявления класса не обязательно.
2) Я писал, что inline можно убирать, просто если его постааить, компилятор с большой вероятностью по цепочке инлайнов заменит `a.x` на `a.get_x()`
Почему-то я подозреваю, что 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))
(это даже приведено в документации). Опять-таки, нет никакой необходимости хранить ссылку на объект в дескрипторе — потому что она будет передана первым же параметром.
Кстати, в плюсах можно попробовать пойти по тому же пути. Правда, имитировать поле класса не получится — язык не позволяет вмешаться в этот синтаксис, но зато можно "закосить" под индексатор (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()))
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, чтобы потом не было мучительно больно при разгребании такого кода).Тут вы ошибаетесь. Еще лет 10 назад все это было реализовано и находится в первых пяти строчках гугла. Вот, например: http://www.codenet.ru/progr/cpp/cpp-properties.php
В javascript объекта класса в чистом виде нет — но их успешно заменяют прототипы. Именно где-то в цепочке прототипов объекта обычно находятся все его методы и сложные свойства.
Не вижу принципиальных отличий от таблицы виртуальных функций. Разве что связывание идет по имени, а не по индексу — да и то можно исправить скрытыми классами (скрытый класс — термин, используемый интерпретатором v8, который используется в хромах и node.js).
В упомянутом вами .NET свойства являются "сахаром" для вызова методов.
Не совсем так. В .NET к геттеру и сеттеру свойства добавляются атрибуты, означающие, что это именно геттер и сеттер свойства.
Если просто создать методы getSomeValue и setSomeValue, то свойство не появится, и компилятор не скомпилирует код вида x.SomeValue.
То, что вы описываете, есть скорее в Java — геттеры и сеттеры никак не помечаются, есть только конвенция именования getSomeValue и setSomeValue.
И для случая Kotlin, в отличие от .NET, скорее можно сказать, что свойства это сахар — наличие и методов getSomeValue и setSomeValue позволяет предположить, что есть свойство SomeValue (хотя, возможно, там тоже чуть сложнее).
Чуть выше я написал про свойства в Kotlin (коли он упомянут в статье).
Хотелось бы дополнительно уточнить некоторые вещи.
Зачем нужны свойства в принципе — понятно. Это и "ленивая" инициализация, и, как выше уже отметили, возможность логгирования, обработки OnChanged, и т.д., главное, чтобы свойство соответствовало конвенции, что оно реализует "легкую логику."
Так вот, отчего же идут постоянные дискуссии, а нужны ли свойства.
На мой взгляд, дело в том, что до сих пор в большинстве языков они были реализованы не до конца.
Две самые важные недоделки:
- Несвязанность на уровне модели backing field и свойства, это две разные сущности, которые разработчик мысленно объединяет и каждый раз вручную пишет их объединение.
В результате, как минимум, код выглядит неаккуратно (как группировать поле и свойство?), как максимум — потенциальные ошибки (часто внутри класса идет обращение то к полю, то к свойству, и нельзя сказать как правильно — если логика геттера усложнится, то внутри нужно обращаться к полю или свойству? — а уже есть разные обращения, которые были написаны без какой-либо системы). - Необходимость каждый раз вручную реализовывать паттерны для некоторых задач.
Например, ленивая инициализация, выдача исключения при попытке чтения еще не инициализированного свойства, проверка допустимости присваиваемого значения, обработка OnChanged, и т.д.
Так вот, именно эти вопросы решены в Kotlin (соответственно, с помощью backing field, видимого только внутри геттера/сеттера, и мощного механизма делегированных свойств) и, насколько я знаю, в таком объеме решены впервые.
Другими, словами, в именно Kotlin мы впервые имеем в полном объеме поддержку свойств на уровне модели языка.
В других языках для реализации полноценных свойств, при определении почти каждого свойства, приходится вручную реализовывать одни и те же паттерны.
Насколько я понимаю, вопрос делегированных частично решался в .NET в механизме DependencyProperties, но реализовано это не в языке и не в платформе в целом, а в подмножестве платформы (WPF). К тому же, чтобы определить в классе Dependency Property, нужно написать дополнительный код, опять же, по определенному паттерну, что отчасти нивелирует то сокращение кода, которое мы получаем с помощью Dependency Property.
Как вместить property в один байт?