Я только что прочитал одну из статей Джоэла, в которых он говорит:
В общем, я должен признать, что я немного боюсь языковых особенностей, которые скрывают вещи . Когда вы видите код
i = j * 5;
… В C вы знаете, по крайней мере, что j умножается на пять, а результаты сохраняются в i.
Но если вы видите тот же фрагмент кода на C ++, вы ничего не знаете. Ничего. Единственный способ узнать, что в действительности происходит в C ++, - это выяснить, что такое типы i и j, что может быть объявлено где-то еще. Это потому, что j может иметь тип, который
operator*
перегружен, и он делает что-то очень остроумное, когда вы пытаетесь его умножить.
(Акцент мой.) Боишься языковых особенностей, которые скрывают вещи? Как ты можешь бояться этого? Разве сокрытие вещей (также известное как абстракция ) не является одной из ключевых идей объектно-ориентированного программирования? Каждый раз, когда вы вызываете метод a.foo(b)
, вы не представляете, что это может сделать. Вы должны выяснить, что это за типы a
и b
что, что может быть объявлено где-то еще. Так стоит ли нам отказываться от объектно-ориентированного программирования, потому что оно слишком много скрывает от программиста?
И чем это j * 5
отличается от того j.multiply(5)
, что вам, возможно, придется написать на языке, который не поддерживает перегрузку операторов? Опять же, вам придется выяснить тип j
и заглянуть внутрь multiply
метода, потому что вот, j
может быть, это тот тип, у которого есть multiply
метод, который делает что-то ужасно остроумное.
«Муахаха, я злой программист, который называет метод multiply
, но на самом деле он абсолютно неясен и не интуитивен, и ему абсолютно нечего делать с умножением». Это сценарий, который мы должны учитывать при разработке языка программирования? Тогда мы должны отказаться от идентификаторов из языков программирования на том основании, что они могут вводить в заблуждение!
Если вы хотите узнать, что делает метод, вы можете взглянуть на документацию или заглянуть внутрь реализации. Перегрузка операторов - просто синтаксический сахар, и я не вижу, как это вообще меняет игру.
Пожалуйста, просветите меня.
Ответы:
Абстракция «скрывает» код, так что вам не нужно беспокоиться о внутренней работе, а часто и о том, что вы не можете их изменить, но целью было не помешать вам взглянуть на него. Мы просто делаем предположения об операторах, и, как сказал Джоэл, это может быть где угодно. Наличие функции программирования, требующей установки всех перегруженных операторов в определенном месте, может помочь найти ее, но я не уверен, что это облегчает ее использование.
Я не вижу, чтобы заставить * делать то, что не очень похоже на умножение, лучше, чем функция Get_Some_Data, которая удаляет данные.
источник
<<
оператор, определенный для потоков, который не имеет ничего общего с битовым сдвигом, прямо в стандартной библиотеке C ++.ИМХО, языковые функции, такие как перегрузка операторов, дают программисту больше возможностей. И, как мы все знаем, с большой силой приходит большая ответственность. Функции, которые дают вам больше возможностей, также дают вам больше возможностей стрелять себе в ногу, и, очевидно, должны использоваться разумно.
Например, это имеет смысл перегружать
+
или*
оператор дляclass Matrix
илиclass Complex
. Каждый сразу узнает, что это значит. С другой стороны, тот факт, что это+
означает, что конкатенация строк не совсем очевидна, хотя Java делает это как часть языка, а STL делает это сstd::string
использованием перегрузки операторов.Еще один хороший пример того, когда перегрузка операторов делает код более понятным, - это умные указатели в C ++. Вы хотите, чтобы умные указатели вели себя как обычные указатели в максимально возможной степени, поэтому имеет смысл перегрузить унарные
*
и->
операторы.По сути, перегрузка операторов - это не что иное, как еще один способ назвать функцию. И есть правило для именования функций: имя должно быть описательным, чтобы сразу было понятно, что делает функция. Точно такое же правило применяется к перегрузке операторов.
источник
*
для них может вызвать путаницу. Возможно, вы могли бы использоватьoperator*()
для точечного продукта иoperator%()
для перекрестного продукта, но я бы не стал делать это для библиотеки общего пользования.A-B
какB-A
либо, и все операторы следуют этому шаблону. Хотя всегда есть одно исключение: когда компилятор может доказать, что это не имеет значения, ему разрешается все переставлять.В Haskell «+», «-», «*», «/» и т. Д. Являются просто (инфиксными) функциями.
Должны ли вы назвать инфиксную функцию «плюс», как в «4 плюс 2»? Почему нет, если дополнение - это то, что делает ваша функция. Должны ли вы назвать вашу «плюс» функцию «+»? Почему нет.
Я думаю, что проблема с так называемыми «операторами» заключается в том, что они в основном напоминают математические операции, и существует не так много способов их интерпретировать, и поэтому существуют большие надежды на то, что делает такой метод / функция / оператор.
РЕДАКТИРОВАТЬ: сделал мою точку зрения более ясным
источник
int
,float
,long long
и что угодно. Так о чем это все?+
для различных встроенных типов чисел, а создают перегрузки, определяемые пользователем. отсюда мой комментарий.Основываясь на других ответах, которые я видел, я могу только заключить, что реальное возражение против перегрузки операторов - это желание сразу же очевидного кода.
Это трагично по двум причинам:
источник
Я несколько согласен.
Если вы пишете
multiply(j,5)
,j
может быть скалярного или матричного типа, что делаетmultiply()
более или менее сложным, в зависимости от того, чтоj
есть. Однако, если вы полностью откажетесь от идеи перегрузки, тогда нужно будет назвать функциюmultiply_scalar()
илиmultiply_matrix()
сделать ее очевидной, что происходит под ней.Есть код, где многие из нас предпочитают его одним способом, и есть код, где большинство из нас предпочли бы его другим способом. Однако большая часть кода попадает в середину между этими двумя крайностями. То, что вы предпочитаете, зависит от вашего опыта и личных предпочтений.
источник
multiply_matrix()
не понравится и универсальное программирование.real_multiply()
или около того. Разработчики часто плохо разбираются в именах и,operator*()
по крайней мере, будут последовательны.operator*()
может сделать что-то глупое,j
это макрос, вычисляющий выражения, включающие пять вызовов функций, и еще много чего. Тогда вы больше не можете сравнивать эти два подхода. Но да, называть вещи хорошо сложно, хотя и стоит того времени, которое требуется.Я вижу две проблемы с перегрузкой оператора.
&&
,||
или,
вы потеряете очки последовательности , которые вытекают из встроенных вариантов этих операторов (а также поведение короткого замыкания логических операторов). По этой причине лучше не перегружать эти операторы, даже если язык это позволяет.operator<<
для потоков.источник
&&
и||
таким образом, который не подразумевает секвенирование, было большой ошибкой (ИМХО, если бы C ++ собирался разрешить их перегрузку, он должен был бы использовать специальный формат «двух функций», при этом требовалась первая функция чтобы вернуть тип, который был неявно конвертируемым в целое число; вторая функция могла бы принимать два или три аргумента, причем «дополнительный» аргумент второй функции был бы типом возврата первого. Компилятор вызывал бы первую функцию и затем, если он вернул ненулевое значение, оцените второй операнд и вызовите в нем вторую функцию.)foo.bar[3].X
обрабатывать выражения вродеfoo
класса, а не требоватьfoo
предоставления члена, который может поддержать подписку и затем выставить участникаX
. Если кто-то хочет форсировать оценку через фактический доступ участника, он написал бы((foo.bar)[3]).X
.Исходя из моего личного опыта, способ Java, позволяющий использовать несколько методов, но не перегружать оператор, означает, что всякий раз, когда вы видите оператор, вы точно знаете , что он делает.
Вы не должны видеть,
*
вызывает ли странный код, но знаете, что это умножение, и оно ведет себя точно так же, как это определено в Спецификации языка Java. Это означает, что вы можете сконцентрироваться на реальном поведении, вместо того чтобы выяснить все, что задает программист.Другими словами, запрещение перегрузки операторов является преимуществом для читателя , а не автора , и, следовательно, облегчает обслуживание программ!
источник
list.get(n)
синтаксисом?std::list
не перегружаетoperator[]
(и не предоставляет никаких других средств индексирования в списке), потому что такой операцией будет O (n), и интерфейс списка не должен предоставлять такую функцию, если вы заботитесь об эффективности. У клиентов может возникнуть соблазн перебирать связанные списки с индексами, что делает ненужными алгоритмы O (n) O (n ^ 2). Вы видите это довольно часто в коде Java, особенно если люди работают сList
интерфейсом, целью которого является полное абстрагирование сложности.time.add(anotherTime)
, вам также придется проверить, правильно ли программист библиотеки реализовал операцию добавления (что бы это ни значило).Одно из различий между перегрузкой
a * b
и вызовомmultiply(a,b)
состоит в том, что последний может быть легко найден. Еслиmultiply
функция не перегружена для разных типов, вы можете точно узнать, что будет делать функция, без необходимости отслеживать типыa
иb
.У Линуса Торвальдса есть интересный аргумент о перегрузке операторов. При разработке ядра Linux, когда большая часть изменений отправляется через патчи по электронной почте, важно, чтобы сопровождающие могли понять, что будет делать патч с несколькими строками контекста вокруг каждого изменения. Если функции и операторы не перегружены, то патч легче прочитать независимо от контекста, поскольку вам не нужно просматривать измененный файл, выясняя, что представляют собой все типы, и проверять перегруженные операторы.
источник
Я подозреваю, что это как-то связано с нарушением ожиданий. Если вы привыкли к C ++, вы привыкли к тому, что поведение оператора не полностью определяется языком, и вы не удивитесь, когда оператор сделает что-то странное. Если вы привыкли к языкам, которые не имеют такой возможности, а затем видите код C ++, вы несете ожидания от этих других языков и можете быть противно удивлены, когда обнаружите, что перегруженный оператор делает что-то странное.
Лично я думаю, что есть разница. Когда вы можете изменить поведение встроенного синтаксиса языка, он становится более непрозрачным для размышлений. Языки, которые не допускают метапрограммирование, синтаксически менее мощны, но концептуально проще для понимания.
источник
Я думаю, что перегрузка математических операторов не является реальной проблемой с перегрузкой операторов в C ++. Я думаю, что перегрузка операторов, которые не должны полагаться на контекст выражения (то есть типа), является «злой». Например, перегруз
,
[ ]
( )
->
->*
new
delete
или даже одинарный*
. У вас есть определенный набор ожиданий от тех операторов, которые никогда не должны меняться.источник
operator[]
, функторы безoperator()
, умные указатели безoperator->
и так далее.[]
всегда должен быть аксессором, подобным массиву, и->
всегда должен означать доступ к члену. Неважно, массив это или другой контейнер, умный указатель или нет.Я прекрасно понимаю, что вам не нравится аргумент Джоэла о сокрытии. И я нет. Действительно, гораздо лучше использовать «+» для таких вещей, как встроенные числовые типы или для ваших собственных, например, для матрицы. Я признаю, что это аккуратно и элегантно, чтобы иметь возможность умножать две матрицы на «*» вместо «.multiply ()». В конце концов, у нас одинаковая абстракция в обоих случаях.
Здесь болит читаемость вашего кода. В реальных случаях, а не в академическом примере умножения матриц. Особенно, если ваш язык позволяет, например, определять операторы, которые изначально не присутствуют в ядре языка
=:=
. На этом этапе возникает множество дополнительных вопросов. О чем этот чертов оператор? Я имею в виду, каков приоритет этой вещи? Что такое ассоциативность? В каком порядкеa =:= b =:= c
действительно выполняется?Это уже аргумент против перегрузки операторов. Все еще не убежден? Проверка правил приоритета заняла у вас не более 10 секунд? Хорошо, пойдем дальше.
Если вы начнете использовать язык, который допускает перегрузку операторов, например тот популярный, чье имя начинается с 'S', вы быстро поймете, что разработчики библиотек любят переопределять операторы. Конечно, они хорошо образованы, они следуют лучшим практикам (здесь нет цинизма), и все их API имеют смысл, когда мы смотрим на них отдельно.
Теперь представьте, что вам нужно использовать несколько API, которые интенсивно используют операторы, перегружающие друг друга в одном куске кода. Или даже лучше - вы должны прочитать какой-то устаревший код, подобный этому. Это когда перегрузка оператора действительно отстой. В основном, если в одном месте много перегруженных операторов, они скоро начнут смешиваться с другими не буквенно-цифровыми символами в коде вашей программы. Они будут смешиваться с не буквенно-цифровыми символами, которые на самом деле не являются операторами, а скорее являются более фундаментальными элементами грамматики языка, которые определяют такие вещи, как блоки и области действия, операторы управления потоком формы или обозначают некоторые мета-вещи. Вам нужно будет надеть очки и подвести глаза на 10 см ближе к ЖК-дисплею, чтобы понять, что такое визуальный беспорядок.
источник
В общем, я избегаю использования перегрузки операторов неинтуитивным способом. То есть, если у меня есть числовой класс, перегрузка * допустима (и приветствуется). Однако, если бы у меня был класс Employee, что бы перегрузка * сделала? Другими словами, операторы перегрузки интуитивно понятны, что облегчает их чтение и понимание.
Приемлемый / Воодушевленные:
Недопустимо:
источник
В дополнение к тому, что уже было сказано, есть еще один аргумент против перегрузки операторов. Действительно, если вы пишете
+
, это очевидно, что вы имеете в виду добавление чего-либо к чему-либо. Но это не всегда так.Сам C ++ является отличным примером такого случая. Как
stream << 1
предполагается читать? поток сдвинут влево на 1? Это совсем не очевидно, если вы явно не знаете, что << в C ++ также пишет в поток. Однако, если бы эта операция была реализована как метод, ни один здравомыслящий разработчик не напишетo.leftShift(1)
, это было бы что-то вродеo.write(1)
.Суть в том, что, делая перегрузку операторов недоступной, язык заставляет программистов задумываться об именах операций. Даже если выбранное имя не является идеальным, все равно труднее неверно истолковать имя, чем знак.
источник
По сравнению с описанными методами, операторы короче, но они также не требуют скобок. Скобки относительно неудобны для ввода. И вы должны сбалансировать их. Всего для любого вызова метода требуется три символа простого шума по сравнению с оператором. Это делает использование операторов очень, очень заманчивым.
Зачем еще кому-то этого хотеть
cout << "Hello world"
?Проблема с перегрузкой заключается в том, что большинство программистов невероятно ленивы, и большинство программистов не могут себе этого позволить.
То, что программистов на C ++ заставляет злоупотреблять перегрузкой операторов, заключается не в их наличии, а в отсутствии более совершенного способа выполнения вызовов методов. И люди не просто боятся перегрузки оператора, потому что это возможно, а потому, что это сделано.
Обратите внимание, что, например, в Ruby и Scala никто не боится перегрузки операторов. Помимо того факта, что использование операторов на самом деле не короче методов, другая причина заключается в том, что Ruby ограничивает перегрузку операторов разумным минимумом, в то время как Scala позволяет объявлять свои собственные операторы, таким образом избегая коллизий.
источник
Причина, по которой перегрузка оператора является пугающей, заключается в том, что существует большое количество программистов, которые никогда даже не ДУМАЮТ, что
*
не означает просто «умножение», в то время как такой метод, какfoo.multiply(bar)
минимум, сразу же указывает этому программисту, что кто-то написал собственный метод умножения , В этот момент они задаются вопросом, почему и идут расследование.Я работал с «хорошими программистами», которые занимали должности высокого уровня, которые создавали бы методы, называемые «CompareValues», которые принимали бы 2 аргумента, применяли значения одного к другому и возвращали логическое значение. Или метод с именем «LoadTheValues», который отправляет в базу данных 3 других объекта, получает значения, выполняет вычисления, изменяет
this
и сохраняет его в базе данных.Если я работаю в команде с этими типами программистов, я сразу же узнаю, чтобы исследовать вещи, над которыми они работали. Если они перегружают оператора, я никак не могу знать, что они это сделали, кроме как предположить, что они сделали, и пойти искать.
В идеальном мире или команде с идеальными программистами перегрузка операторов, вероятно, фантастический инструмент. Однако мне еще предстоит работать в команде идеальных программистов, поэтому это страшно.
источник