Pull to refresh

Comments 8

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

Полтора года работал над игровым проектом, в котором, конечно, активно использовались операции над векторами, но не использовались перегруженные операторы. Я был озадачен, так как считал что перегруженные операторы это круто — но мне сказали что так яснее происходящее в коде. Дома я писал свой pet-движок, в котором использовал перегруженные операторы… И, знаете, в какой-то момент (а именно — при написании выражения, в котором участвовало больше двух операторов) я понял, что ребята на работе были правы. Действительно, когда операция записывается в виде функции понимание яснее. Помимо этого, при использовании функций [а не операторов] можно передавать вектора по ссылке и не порождать временные объекты (я понимаю, copy eligion, оптимизации компиляции и всё такое, но семантика кода есть семантика кода).

П.С.: Всё сказанное выше касалось С++. Возможно, в Swifth будет по-другому. На мой взгляд, указанные проблемы выглядят как универсальные.
Запутать действительно можно что угодно, но с разной степенью усилий. Предлагаю обсудить:

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

2) на сколько мне известно, в Cpp способы передачи аргументов и возврата значений регулируются идентично и для функций и для операторов: хоть по ссылке, хоть по значению. Единственная разница в том, что операторные функции можно вызывать и явно как функцию (a.operator+(&b)) и неявно через операторную запись (a+b). При чем, компилятор, вроде бы, всегда все сводит к первому варианту. А вот для методов/функций доступен только первый вариант нотации.

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

В общем, предлагаю такой пример для упражнения:
float t, k; // числа
Point2 a, b, c; // векторы с только двумя координатами икс и игрек
Trasfoator M; // какой-то оператор
a = b*t + c*k*M; // запись с перегрузкой

a = b*t + c*k*M;


Если такой код будет где-то не рядом с объявлением переменных — трудно догадаться что там вообще происходит. Нужно знать типы объектов… Впрочем, можно прикинуть как это будет выглядеть в случаи с записью без операторов:

a.assign(b);
a.mul(t);

Point tmp;
tmp.set(c).scolar_mul(k).matrix_mul(M);

a.sum(tmp);


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

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

И все же, тут есть над чем поломать копья.



Если такой код будет где-то не рядом с объявлением переменных

Объявление нелокальных переменных с неинформативными именами — это нередко целый букет систематических проблем уже само по себе. Это один из первых уроков, который я вынес когда-то из книжки «Чистый код» (кажется так она называлась), а потом и лично убедился, что мне содержательное именование переменных снимает львиную долю проблем.
Соглашусь так же, что реализация через методы дает дополнительный вариант «защиты» через содержательное именование самих методов. Такая опция для операторов кажется недоступна.

Но стоит ли так радикально ставить вопрос: либо только методы, либо только операторы?
Мне кажется, что истина посередине. Например: можно реализовать только однородные операторы (сложение/вычитание), которые подразумевают оба операнда однотипными.
Сделает это код более компактным, чем только реализуя через методы? Возможно.
Будет такой код более понятным относительно типов с операндами? Так же возможно.
Если по мне, то гибридный подход уже дает как минимум «probably Win probably Win»



можно прикинуть как это будет выглядеть в случаи с записью без операторов

Спасибо! Получилось, как у меня.
Здесь есть двоякий момент.
  1. 1) если создание временного объекта неизбежно (а так случается), то это придется делать вручную. В итоге:
    кол-во действий увеличится (объект нужно объявить и создать),
    способов ошибиться прибавиться (надо будет при поддержке кода отслеживать большее число элементов кода),
    читаемость кода ухудшится (мы будем видеть доп.информацию о том, что делает компилятор в перемешку с тем, что собственно отражает логику замысла разработчика).
    Но соптимизировать будет нечего.
  2. 2) а вот если создание временного объекта можно избежать, то это станет несколько очевиднее, чем в операторной записи. Например:
    a = b+c

    выполняет на одну операцию больше, чем
    a=b
    a+=c

    Но это друга уже дилемма: на этапе Отладки предпочтительнее видеть Что хотим Мы сделать, а на этапе Оптимизации (если она нужна в данном куске кода) Как это будет Реализовывать Компилятор.
    Бытует мнение, что всему свое время: для упрощения Отладки желательно иметь максимально понятную и прозрачную реализацию Задуманной логики, а на этапе Оптимизации желательно иметь Отлаженныйоттестированный) код, который заменяется на аналогичный оптимизированный, а аналогичность Отлаженного и Оптимизированного кода опять же тестится достаточно легко и элегантно… но это друга тема.


В любом случае! Если не касаться проблем из-за оптимизации во время отладки, то и тут есть компромисы вместо крайностей: если не определять бинарные операторы, а только унарные с присвоением, то мы получим невозможность неявного создания временных объектов и это будет, как я понимаю, полная аналогия с подходом через методы.
Причем, это тоже можно реализовать не радикально: либо выкидываем бинарные операторы, либо используем, а гибко:
объявляем и полный набор операторов, но неоптимальные/медленные/итд выносим в блок или файл, который откючается (у меня автоматически), если компиляция переводиться в режим Оптимизации.
То есть, пока отлаживаем — пишем нормальные однострочные понятные формулы почти в том виде, как они в тетрадке/презентации/учебнике записаны.
А когда оптимизируем — компилятор перестанет понимать некоторые места, и мы их заменяем на аналогичные (в чем помогут тесты) развернутые/оптимизированные/использующие_регистры/итд реализации.



Но тут прозрачны все действия, которые происходят с объектами.

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



Ну и еще пара моментов, отчасти как повторение-переформулировка
  1. Наверное не стоит на уровне глобальной идеологии унифицировать противостояние операторов и методов
    Для некоторых мега-классов операторы могут только вредить, не иметь внятного смысла или хотя бы возможности быть оптимальными не только в компиляции, но и в плане читабельности.
    А для некоторых нано-структур даже Избегание создания временных объектов — это больше зло, чем накладные расходы из-за их использования, особенно, если эти классы-структуры могут целиком передаваться в паре регистров процессора.
  2. По моему продолжительному, но скромному опыту, складывается впечатление, что Оптимизировать во время Отладки (тем более Разработки) — это не очень прямой путь: для Отладки нужна прозрачная просматриваемая логика, а для Оптимизации нужен Отлаженный модуль/приложение, которое вот теперь действительно предстоит закодировать в самом прямом смысле!!


Кстати, считаю, что использовать в группе общий стандарт поддержи кода — это все же первоочередная ценность во многих случаях, чем преимущество форм записи.
Sign up to leave a comment.

Articles