какие операторы перегружать нельзя
Перегрузка операторов в C++. Основы
В C++ этого ограничения нет — мы можем перегрузить практически любой известный оператор. Возможностей не счесть: можно выбрать любую комбинацию типов операндов, единственным ограничением является необходимость того, чтобы присутствовал как минимум один операнд пользовательского типа. То есть определить новый оператор над встроенными типами или переписать существующий нельзя.
Когда стоит перегружать операторы?
Приведём хороший и плохой примеры перегрузки операторов. Вышеупомянутое сложение матриц — наглядный случай. Здесь перегрузка оператора сложения интуитивно понятна и, при корректной реализации, не требует пояснений:
Примером плохой перегрузки оператора сложения будет сложение двух объектов типа «игрок» в игре. Что имел в виду создатель класса? Каким будет результат? Мы не знаем, что делает операция, и поэтому пользоваться этим оператором опасно.
Как перегружать операторы?
Большую часть операторов можно перегрузить как методами класса, так и простыми функциями, но есть несколько исключений. Когда перегруженный оператор является методом класса, тип первого операнда должен быть этим классом (всегда *this ), а второй должен быть объявлен в списке параметров. Кроме того, операторы-методы не статичны, за исключением операторов управления памятью.
13 сентября – 9 октября, Санкт-Петербург и онлайн, Беcплатно
При перегрузке оператора в методе класса он получает доступ к приватным полям класса, но скрытая конверсия первого аргумента недоступна. Поэтому бинарные функции обычно перегружают в виде свободных функций. Пример:
Когда унарные операторы перегружаются в виде свободных функций, им доступна скрытая конверсия аргумента, но этим обычно не пользуются. С другой стороны, это свойство необходимо бинарным операторам. Поэтому основным советом будет следующее:
Реализуйте унарные операторы и бинарные операторы типа “X=” в виде методов класса, а прочие бинарные операторы — в виде свободных функций.
Какие операторы можно перегружать?
Мы можем перегрузить почти любой оператор C++, учитывая следующие исключения и ограничения:
В следующей части вашему вниманию будут представлены перегружаемые операторы C++, в группах и по отдельности. Для каждого раздела характерна семантика, т.е. ожидаемое поведение. Кроме того, будут показаны типичные способы объявления и реализации операторов.
Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.
Перейти к регистрации
Урок №130. Введение в перегрузку операторов
Обновл. 13 Сен 2021 |
Из урока №102 мы уже знаем, что перегрузка функций обеспечивает механизм создания и выполнения вызовов функций с одним и тем же именем, но с разными параметрами. Это позволяет одной функции работать с несколькими разными типами данных (без необходимости придумывать уникальные имена для каждой из функций).
В языке C++ операторы реализованы в виде функций. Используя перегрузку функции оператора, вы можете определить свои собственные версии операторов, которые будут работать с разными типами данных (включая классы). Использование перегрузки функции для перегрузки оператора называется перегрузкой оператора.
Операторы, как функции
Рассмотрим следующий фрагмент:
Теперь рассмотрим следующий фрагмент:
Теперь рассмотрим, что произойдет, если мы попытаемся добавить два объекта класса:
Вызов перегруженных операторов
При обработке выражения, содержащего оператор, компилятор использует следующие алгоритмы действий:
Если все операнды являются фундаментальных типов данных, то вызывать следует встроенные соответствующие версии операторов (если таковые существуют). Если таковых не существует, то компилятор выдаст ошибку.
Если какой-либо из операндов является пользовательского типа данных (например, объект класса или перечисление), то компилятор будет искать версию оператора, которая работает с таким типом данных. Если компилятор не найдет ничего подходящего, то попытается выполнить конвертацию одного или нескольких операндов пользовательского типа данных в фундаментальные типы данных, чтобы таким образом он мог использовать соответствующий встроенный оператор. Если это не сработает — компилятор выдаст ошибку.
Ограничения в перегрузке операторов
Во-первых, почти любой существующий оператор в языке C++ может быть перегружен. Исключениями являются:
оператор разрешения области видимости ( :: );
Во-вторых, вы можете перегрузить только существующие операторы. Вы не можете создавать новые или переименовывать существующие. Например, вы не можете создать оператор ** для выполнения операции возведения в степень.
В-третьих, по крайней мере один из операндов перегруженного оператора должен быть пользовательского типа данных. Это означает, что вы не можете перегрузить operator+() для выполнения операции сложения значения типа int со значением типа double. Однако вы можете перегрузить operator+() для выполнения операции сложения значения типа int с объектом класса Mystring.
В-четвертых, изначальное количество операндов, поддерживаемых оператором, изменить невозможно. Т.е. с бинарным оператором используются только два операнда, с унарным — только один, с тернарным — только три.
Наконец, все операторы сохраняют свой приоритет и ассоциативность по умолчанию (независимо от того, для чего они используются), и это не может быть изменено.
Вам нужно будет явно заключать в скобки часть с возведением в степень (например, 2 + (5 ^ 2) ) каждый раз, когда вы хотите, чтобы она выполнялась первой, что очень легко забыть и, таким образом, наделать ошибок. Поэтому проводить подобные эксперименты не рекомендуется.
Примечание: В языке C++ для возведения в степень используется функция pow() из заголовочного файла cmath. В примере, приведенном выше, с выполнением выражения 2 + 5 ^ 2 в языке C++, имеется в виду, что вы перегрузите побитовый оператор XOR ( ^ ) для выполнения операции возведения в степень.
Правило: При перегрузке операторов старайтесь максимально приближенно сохранять функционал операторов в соответствии с их первоначальными применениями.
Для чего использовать перегрузку операторов? Вы можете перегрузить оператор + для соединения объектов вашего класса String или для выполнения операции сложения двух объектов вашего класса Fraction. Вы можете перегрузить оператор для вывода вашего класса на экран (или записи в файл). Вы можете перегрузить оператор равенства ( == ) для сравнения двух объектов класса и т.д. Подобные применения делают перегрузку операторов одной из самых полезных особенностей языка C++, так как это упрощает процесс работы с классами и открывает новые возможности.
Перегрузка операторов в C++
Доброго времени суток!
Желание написать данную статью появилось после прочтения поста Перегрузка C++ операторов, потому что в нём не были раскрыты многие важные темы.
Самое главное, что необходимо помнить — перегрузка операторов, это всего лишь более удобный способ вызова функций, поэтому не стоит увлекаться перегрузкой операторов. Использовать её следует только тогда, когда это упростит написание кода. Но, не настолько, чтобы это затрудняло чтение. Ведь, как известно, код читается намного чаще, чем пишется. И не забывайте, что вам никогда не дадут перегрузить операторы в тандеме со встроенными типами, возможность перегрузки есть только для пользовательских типов/классов.
Синтаксис перегрузки
В данном случае, оператор оформлен как член класса, аргумент определяет значение, находящееся в правой части оператора. Вообще, существует два основных способа перегрузки операторов: глобальные функции, дружественные для класса, или подставляемые функции самого класса. Какой способ, для какого оператора лучше, рассмотрим в конце топика.
В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, то вы сами решаете как интерпретировать результат вычисления оператора).
Перегрузка унарных операторов
Рассмотрим примеры перегрузки унарных операторов для определенного выше класса Integer. Заодно определим их в виде дружественных функций и рассмотрим операторы декремента и инкремента:
Теперь вы знаете, как компилятор различает префиксные и постфиксные версии декремента и инкремента. В случае, когда он видит выражение ++i, то вызывается функция operator++(a). Если же он видит i++, то вызывается operator++(a, int). То есть вызывается перегруженная функция operator++, и именно для этого используется фиктивный параметр int в постфиксной версии.
Бинарные операторы
Рассмотрим синтаксис перегрузки бинарных операторов. Перегрузим один оператор, который возвращает l-значение, один условный оператор и один оператор, создающий новое значение (определим их глобально):
Во всех этих примерах операторы перегружаются для одного типа, однако, это необязательно. Можно, к примеру, перегрузить сложение нашего типа Integer и определенного по его подобию Float.
Аргументы и возвращаемые значения
Оптимизация возвращаемого значения
При создании новых объектов и возвращении их из функции следует использовать запись как для вышеописанного примера оператора бинарного плюса.
Честно говоря, не знаю, какая ситуация актуальна для C++11, все рассуждения далее справедливы для C++98.
На первый взгляд, это похоже на синтаксис создания временного объекта, то есть как будто бы нет разницы между кодом выше и этим:
Но на самом деле, в этом случае произойдет вызов конструктора в первой строке, далее вызов конструктора копирования, который скопирует объект, а далее, при раскрутке стека вызовется деструктор. При использовании первой записи компилятор изначально создаёт объект в памяти, в которую нужно его скопировать, таким образом экономится вызов конструктора копирования и деструктора.
Особые операторы
В C++ есть операторы, обладающие специфическим синтаксисом и способом перегрузки. Например оператор индексирования []. Он всегда определяется как член класса и, так как подразумевается поведение индексируемого объекта как массива, то ему следует возвращать ссылку.
Оператор запятая
В число «особых» операторов входит также оператор запятая. Он вызывается для объектов, рядом с которыми поставлена запятая (но он не вызывается в списках аргументов функций). Придумать осмысленный пример использования этого оператора не так-то просто. Хабраюзер AxisPod в комментариях к предыдущей статье о перегрузке рассказал об одном.
Оператор разыменования указателя
Перегрузка этих операторов может быть оправдана для классов умных указателей. Этот оператор обязательно определяется как функция класса, причём на него накладываются некоторые ограничения: он должен возвращать либо объект (или ссылку), либо указатель, позволяющий обратиться к объекту.
Оператор присваивания
Оператор присваивания обязательно определяется в виде функции класса, потому что он неразрывно связан с объектом, находящимся слева от «=». Определение оператора присваивания в глобальном виде сделало бы возможным переопределение стандартного поведения оператора «=». Пример:
Как можно заметить, в начале функции производится проверка на самоприсваивание. Вообще, в данном случае самоприсваивание безвредно, но ситуация не всегда такая простая. Например, если объект большой, можно потратить много времени на ненужное копирование, или при работе с указателями.
Неперегружаемые операторы
Некоторые операторы в C++ не перегружаются в принципе. По всей видимости, это сделано из соображений безопасности.
Перегрузка операторов / FAQ C++
Что там с перегрузкой операторов?
Она позволяет вам предоставить интуитивно понятный интерфейс для пользователей вашего класса, а также позволяет шаблонам одинаково хорошо работать с классами и со встроенными/внутренними типами.
Перегрузка операторов позволяет операторам C/C++ иметь определяемое пользователем значение для определяемых пользователем типов (классов). Перегруженные операторы – это синтаксический сахар для вызовов функций:
Каковы преимущества перегрузки операторов?
Перегружая в классе стандартные операторы, вы можете использовать интуицию пользователей этого класса. Это позволяет пользователям программировать на языке предметной области, а не на языке машины.
Конечная цель – сократить как кривую обучения, так и количество брака.
Можно увидеть примеры перегрузки операторов?
Вот несколько из многих примеров перегрузки операторов:
Но перегрузка операторов делает мой класс уродливым; разве она не должна сделать мой код более понятным?
Перегрузка операторов облегчает жизнь пользователям класса, а не его разработчикам!
Рассмотрим следующий пример.
Некоторым людям не нравится ключевое слово operator или несколько странный синтаксис, связанный с ним в теле самого класса. Но синтаксис перегрузки операторов не должен облегчать жизнь разработчику класса. Предполагается, что он облегчит жизнь пользователям класса:
Помните: в мире, ориентированном на повторное использование, обычно будет много людей, которые используют ваш класс, но только один человек, который создает его (вы сами); поэтому вы должны делать то, что приносит пользу многим, а не немногим.
Какие операторы можно/нельзя перегружать?
Ниже показан пример оператора индекса (он возвращает ссылку). Сначала без перегрузки оператора:
А ниже показана та же логика с перегрузкой оператора:
Программистом может быть перегружено большинство операторов. Исключения составляют:
sizeof нельзя перегружать, потому что от него неявно зависят встроенные операции, такие как инкрементирование указателя в массиве. Рассмотрим пример:
Таким образом, программист не может придать sizeof(X) новое и иное значение без нарушения основных правил языка.
Эту проблему можно решить несколькими способами. До сих пор в области стандартизации не было очевидно, какой путь лучше всего. Для получения дополнительной информации смотрите D&E.
Могу ли я определять свои операторы?
Извините, но нет. Возможность рассматривалась несколько раз, но каждый раз решалось, что вероятные проблемы перевешивают вероятные преимущества.
Могу ли я создать operator** для операций «возведения в степень»?
Если вы сомневаетесь, считайте, что x ** y совпадает с x * (*y) (другими словами, компилятор предполагает, что y является указателем). Кроме того, перегрузка оператора – это просто синтаксический сахар для вызовов функций. Хотя этот синтаксический сахар может быть очень сладким, он не добавляет ничего фундаментального. Я предлагаю вам перегрузить pow(base, exponent) (версия с двойной точностью находится в ).
Кстати, operator^ может работать для возведения в степень, за исключением того, что у него неправильный приоритет и ассоциативность.
В предыдущих ответах FAQ говорится, какие операторы я могу переопределить; но какие операторы я должен переопределить?
Определяющий фактор: не вводите пользователей в заблуждение.
Помните цель перегрузки операторов: снизить стоимость и уровень дефектов в коде, который использует ваш класс. Если вы создаете операторы, которые сбивают с толку ваших пользователей (потому что они крутые; потому что они делают код быстрее; потому что вам нужно доказать себе, что вы можете сделать это; неважно почему), вы пошли против всех причин использования перегрузки операторов.
Есть ли рекомендации / «практические правила» для перегрузки операторов?
Вот несколько рекомендаций / практических правил (но обязательно прочитайте предыдущий ответ FAQ, прежде чем читать этот список):
Предостережение: этот список не является исчерпывающим. Это означает, что есть и другие пункты, которые вы можете считать «пропущенными». Я знаю.
Предостережение: этот список содержит рекомендации, а не жесткие правила. Это означает, что почти все пункты имеют исключения, и большинство из этих исключений явно не указано. Я знаю.
Почему интерфейс моего класса Matrix не должен выглядеть как массив массивов?
В качестве примера того, когда физическая компоновка имеет существенное значение: в проекте необходимо получать доступ к элементам матрицы в столбцах (то есть, алгоритм обращается ко всем элементам в одном столбце, затем к элементам в другом и т.д.), и если физическая структура является строковой, доступ может «опережать кеш». Например, если размер строк почти равен размеру кеш-памяти процессора, машина может получать «промах кеша» (cache miss) почти при каждом доступе к элементу. В этом конкретном проекте мы получили повышение производительности на 20% за счет изменения сопоставления с логической компоновки (строка, столбец) на физическую компоновку (столбец, строка).
Конечно, есть много подобных примеров из численных методов, и разреженные матрицы – это совершенно другое измерение в этом вопросе. Поскольку, как правило, с использованием подхода operator() реализовать разреженную матрицу или поменять местами порядок строк/столбцов проще, подход operator() ничего не теряет и может что-то получить – у него нет недостатков, и есть потенциальные преимущества.
Я всё еще не понимаю. Почему интерфейс моего класса Matrix не должен выглядеть как массив массивов?
По тем же причинам, по которым вы инкапсулируете свои структуры данных, и по той же причине, по которой вы проверяете параметры, чтобы убедиться, что они корректны.
Суть предыдущих двух ответов FAQ заключается в том, что m(i,j) дает вам чистый и простой способ проверить все параметры и скрыть (и, следовательно, при желании изменить) внутреннюю структуру данных. В мире уже слишком много открытых структур данных и слишком много параметров, выходящих за границы, и это стоит слишком больших денег и вызывает слишком много задержек и слишком много проблем.
Теперь все знают, что вы другой. Вы обладаете ясновидением и прекрасно знаете будущее, и вы знаете, что никто никогда не получит никакой пользы от изменения внутренней структуры данных вашей матрицы. Кроме того, вы хороший программист, в отличие от тех бездельников, которые иногда передают неправильные параметры, поэтому вам не нужно беспокоиться о неприятных мелочах, таких как проверка параметров. Но даже если вам не нужно беспокоиться о расходах на поддержку (никому не нужно менять ваш код), могут быть еще один или два программиста, которые еще не совсем идеальны. Для них затраты на поддержку высоки, ошибки реальны, а требования меняются. Верите вы или нет, но время от времени им нужно (лучше присядьте) менять свой код.
Возможно, я утрирую. Но в этом был смысл. Дело в том, что инкапсуляция и проверка параметров – не костыли для слабых. Разумно использовать методы, которые упрощают инкапсуляцию и/или проверку параметров. Синтаксис m(i,j) – один из таких приемов.
Если вы просто хотите проверить параметры, просто убедитесь, что внешний operator[] возвращает объект, а не необработанный массив, тогда operator[] этого объекта сможет проверить свой параметр обычным способом. Помните, что это может замедлить вашу программу. В частности, если эти подобные массивам, внутренние объекты в конечном итоге выделяют собственный блок памяти для своей строки матрицы, накладные расходы на производительность для создания/уничтожения ваших объектов матриц могут резко возрасти. Теоретическая стоимость по-прежнему составляет O (строки × столбцы), но на практике накладные расходы распределителя памяти ( new или malloc ) могут быть намного больше, чем что-либо еще, и другие затраты могут стать незаметными на фоне этих накладных расходов. Например, в двух самых известных компиляторах C++ метод с отдельным выделением памяти для каждой строки был в 10 раз медленнее, чем метод «одно выделение памяти для всей матрицы». 10% – это одно, 10х – другое.
Если вы хотите проверить параметры без вышеуказанных накладных расходов и/или если вы хотите инкапсулировать (и, возможно, изменить) внутреннюю структуру данных матрицы, выполните следующие действия:
Как мне разрабатывать свои классы, снаружи (сначала интерфейсы) или изнутри (сначала данные)?
Хороший интерфейс обеспечивает упрощенное представление, которое выражается в словарном запасе пользователя. В случае объектно-ориентированного программирования интерфейс обычно представляет собой набор общедоступных методов либо одного класса, либо узкой группы классов.
Начиная с точки зрения пользователя, нам может потребоваться, чтобы наш класс LinkedList поддерживал операции, похожие на доступ к элементам массива с использованием арифметики указателей:
Вот методы, которые явно являются встраиваемыми (и, вероятно, находятся в том же заголовочном файле):
Заключение: связанный список имеет два разных типа данных. Значения элементов, хранящихся в связанном списке, находятся в зоне ответственности пользователя связанного списка (и только пользователя; сам связанный список не пытается запретить пользователям изменять третий элемент на 5), а данные инфраструктуры связанного списка (указатели next и т.д.) и их значения находятся в зоне ответственности связанного списка (и только связанного списка; например, связанный список не позволяет пользователям изменять (или даже просматривать!) различные указатели next ).
Таким образом, единственным методами get() / set() были получение и установка значений элементов связанного списка, а не инфраструктуры связанного списка. Поскольку связанный список скрывает инфраструктурные указатели и тому подобное, он может обеспечивать очень высокую надежность в отношении этой инфраструктуры (например, если бы это был двусвязный список, он мог бы гарантировать, что каждый прямой указатель соответствовал обратному указателю из следующего объекта Node ).
Через фиктивный параметр.
Обратите внимание на разные типы возвращаемых значений: префиксная версия возвращает результат по ссылке, постфиксная версия – по значению. Если причина этого была вам не очевидна сразу, то ситуация прояснится после того, как вы увидите определения (и после того, как вы вспомните, что y = x++ и y = ++x присваивают y разные значения).
Другой вариант для постфиксной версии – ничего не возвращать:
Однако вы не должны заставлять постфиксную версию возвращать объект this по ссылке; вы были предупреждены.
Вот как вы используете эти операторы:
Предполагая, что возвращаемые типы не являются «недействительными», вы можете использовать их в более крупных выражениях:
Очевидно, что когда i++ появляется как часть более крупного выражения, это другое дело: он используется потому, что это единственное логически правильное решение, а не потому, что это старая привычка, которую вы приобрели при программировании на C.
Перегрузка операторов
operator Ключевое слово объявляет функцию, указывающую, какой оператор-Symbol означает при применении к экземплярам класса. Это дает оператору более одного значения — «перегружает» его. Компилятор различает разные значения оператора, проверяя типы его операндов.
Синтаксис
тип operator operator-символ ( parameter-list )
Remarks
Функцию большинства встроенных операторов можно переопределить глобально или для отдельных классов. Перегруженные операторы реализуются в виде функции.
Имя перегруженного оператора — operator x, где x — это оператор, как показано в следующей таблице. Например, для перегрузки оператора сложения необходимо определить функцию с именем operator +. Аналогично, чтобы перегрузить оператор сложения и присваивания, += Определите функцию с именем operator + =.
Переопределяемые операторы
Оператор | Имя | Тип |
---|---|---|
, | Запятая | Двоичные данные |
! | Логическое НЕ | Унарный |
!= | Неравенство | Двоичные данные |
% | Modulus | Двоичные данные |
%= | Назначение модуля | Двоичные данные |
& | Побитовое И | Двоичные данные |
& | Взятие адреса | Унарный |
&& | Логическое И | Двоичные данные |
&= | Назначение побитового И | Двоичные данные |
( ) | Вызов функции | — |
( ) | Оператор приведения | Унарный |
* | Умножение | Двоичные данные |
* | Разыменование указателя | Унарный |
*= | Присваивание умножения | Двоичные данные |
+ | Сложение | Двоичные данные |
+ | Унарный плюс | Унарный |
++ | Шаг 1 | Унарный |
+= | Присваивание сложения | Двоичные данные |
— | Вычитание | Двоичные данные |
— | Унарное отрицание | Унарный |
— | Уменьшить 1 | Унарный |
-= | Присваивание вычитания | Двоичные данные |
-> | Выбор члена | Двоичные данные |
— >* | Выбор указателя на член | Двоичные данные |
/ | Отдел | Двоичные данные |
/= | Присваивание деления | Двоичные данные |
Больше | Двоичные данные | |
>= | Больше или равно | Двоичные данные |
>> | Сдвиг вправо | Двоичные данные |
>>= | Сдвиг вправо и присваивание | Двоичные данные |
[ ] | Индекс массива | — |
^ | Исключающее ИЛИ | Двоичные данные |
^= | Исключающее ИЛИ/присваивание | Двоичные данные |
| | Побитовое ИЛИ | Двоичные данные |
|= | Назначение побитового включающего ИЛИ | Двоичные данные |
|| | Логическое ИЛИ | Двоичные данные |
Дополнение до единицы | Унарный | |
delete | Удаление | — |
new | Создать | — |
операторы преобразования | операторы преобразования | Унарный |
Существует 1 две версии унарных операторов инкремента и декремента: добавочное и инкрементное.