Языки программирования используют разнообразные операторы для выполнения операций. В большинстве из них операторы имеют жесткую привязку к типам. Так, в Java сложение с оператором «+» допустимо только для целочисленных значений, чисел с плавающей запятой и строк. Если определить свои классы для математических объектов, разработчику удастся сложить их. Только для вызова соответствующего метода необходимо использовать специальные команды.
В C++ подобных ограничений нет. У данного языка программирования поддерживается так называемая перегрузка. Она возможна почти для каждого известного оператора. Это предоставляет целое поле дополнительных возможностей разработчику.
Далее предстоит познакомиться с перегрузкой операторов в C++ поближе. Необходимо не только выяснить, что это за операция такая, но и освоить принципы ее реализации. Предложенная информация окажется полезной как программистам-новичкам, так и более опытным разработчикам.
Определение
Перегрузка операторов – это один из способов реализации полиморфизма. Он заключается в возможности одновременного существования в одной и той же области видимости нескольких различных вариантов применения операторов, имеющих одно и то же имя, но различающихся типами параметров, к которым он применяется.
Перегрузка (или operator overloading) – операция, позволяющая определить для объектов классов встроенные операторы. Она широко используется разработчиками. Применяется для переопределения действий операторов языка в процессе работы с разнообразными (включая пользовательские) типами. Возможность применения встроенных операторов языка к разным типам данных.
Когда использовать процедуру
В процессе разработки программного обеспечения необходимо запомнить одно правило – перегружать операторы нужно тогда, когда соответствующая процедура имеет смысл: если он очевиден и не несет в себе никаких скрытых «сюрпризов» для программиста.
Перегруженные operators должны вести себя так же, как и их «базовые» версии. Исключения допустимы, но лишь тогда, когда они сопровождаются понятными разработчику объяснениями.
Примером подобной ситуации могут служить операторы «<<» и «>>» стандартной библиотеки C++ iostream. Они явно ведут себя не как обычный битовый сдвиг.
Чтобы лучше понимать, когда лучше применять перегрузку, стоит рассмотреть хороший и плохой примеры реализации соответствующей операции. Наглядным случаем является сложение матриц. Здесь перегрузка сложения является интуитивно понятной. Если грамотно реализовать процедуру, она не будет требовать пояснений.
Пример плохой перегрузки сложения – это сложение двух объектов типа «игрок» в игровом проекте. Не совсем понятно, что разработчик имел в виду для этих классов. Результат непонятен, непредсказуем. Что делает соответствующая операция – огромный вопрос. Именно поэтому использование соответствующего оператора – опасное решение.
Синтаксис
Синтаксис рассматриваемой процедуры напоминает определение функции с именем operator@, где @ – это идентификатор оператора. Вот наглядный пример реализации процесса:
Обычно операторы (за исключением условных) будут возвращать объект или ссылку на тип, к которому относятся его аргументы.
Что нельзя перегружать
Перегрузка операций предусматривает некоторые особенности и ограничения, о которых должен помнить каждый разработчик. Почти любой operator C++ может быть перегружен. Вот ограничения и нюансы, о которых должен помнить каждый программист:
- определить новый оператор нельзя (operator**);
- нельзя перегружать тернарный оператор, доступ ко вложенным именам, доступ к полям, доступ к полям по указателю;
- рассматриваемая процедура не допускается для операторов каста;
- нельзя перегружать operator sizeof и operator typeid;
- количество операндов, их ассоциативность и порядок выполнения определяется стандартной версией;
- функция operator должна быть или нестатической (функцией-членом), или глобальной свободной функцией, или дружественной;
- если функция дружественная, бинарный оператор имеет два аргумента, унарный – только один;
- при работе с нестатической функцией-членом бинарный оператор имеет один аргумент, унарный – вовсе их не имеет;
- минимум один операнд должен быть пользовательского типа, исключая operator typedef.
Только в качестве методов можно перегружать: присваивание, доступ к полям по указателю, вызов функции, доступ по индексу, а также операторы конверсии и управления памятью, доступ к указателю на поле по указателю.
Способы реализации
Перегрузка может быть реализована в C++ несколькими способами. Всего их три:
- через методы класса;
- при помощи дружественных функций для класса;
- посредством обычных функций.
Далее каждый вариант будет рассмотрен отдельно. Также предстоит изучить перегружаемые операторы C++ более подробно.
Реализация через функцию
Для использования данного приема необходимо объявить функцию, которая будет переопределять operator тела класса. Соответствующая перегрузка не может обращаться к членам класса напрямую. Воспользоваться подобной операцией можно за счет геттеров.
Такой прием рекомендуется использовать вместе операторной перегрузки через дружественные функции тогда, когда для этого нет никакой необходимости в добавлении дополнительных геттеров в класс.
Реализация через дружественные функции
Такой подход напоминает предыдущий вариант. Он отличается от ранее изученной концепции тем, что задействована будет не обычная функция, а дружественная классу. Такой прием позволяет обращаться к членам класса напрямую. Геттеры для этого не нужны.
Дружественные функции могут быть определены внутри заданного класса.
Выше можно увидеть наглядный пример реализации упомянутого метода в C++.
Через методы класса
Еще один способ операторной перегрузки в C++ – это использование методов класса.
В этом случае у функции-метода вместо первого операнда появляется неявный параметр – указатель на объект класса. Выше – наглядный пример того, как это будет выглядеть в программном коде. Теперь можно более подробно рассмотреть операторы, для которых в C++ допустима перегрузка.
Что можно перегрузить – особенности процесса
Далее будут представлены operators, которые можно перегружать. Каждый из них поддерживает характерную семантику (ожидаемое поведение) и типичные способы объявления/реализации.
В первом приведенном примере X – это пользовательский тип, для которого будет реализован operator. T – необязательный тип, пользовательский или встроенный. В качестве параметров бинарного оператора выступают lhs и rhs. Если operator объявляется в качестве метода класса, он получит префикс x::.
Operator =
Operator = – это присваивание. Его семантика:
a = b
Соответствующая запись указывает на то, что значение/состояние b будет передано a. В процессе реализации возвращается ссылка на a. За счет этого приема можно создавать цепочки вроде c = a = b.
Operator = определяется справа налево. Он правоассоциативен в отличие от большинства других операторов C++. Это значит, что a = b = c означает a = (b = c).
Типичным объявлением копирования является запись: X& X:: operator = (X const& rhs).
Перемещения, начиная с C++ 11, имеют семантику присваивания a = temporary(). Значение или состояние правой величины присваивается a за счет перемещения содержимого. На a будет возвращаться ссылка.
Ввод и вывод
Operator << может перегружаться, чтобы добиться более удобного вывода сложных структур в поток вывода.
Выше – наглядный пример реализации соответствующей операции. аналогичным образом можно перегрузить operator >>. В этом случае вместо std:: ostream необходимо использовать std::istream.
Унарные операторы
В случае с унарными operators функция переопределения будет принимать на выход только один операнд. Operator + чаще всего ничего не делает, поэтому на практике он почти не используется. Operator – осуществляет возврат аргумента с противоположным знаком.
Из-за того, что функция перегрузки принимает только один операнд на вход, рекомендуется осуществлять рассматриваемую операцию посредством метода класса. Выше представлен наглядный пример реализации соответствующего приема.
Метод должен быть объявлен в качестве константного. Это необходимо из-за того, что объект класса не меняется, а возвращается новый экземпляр класса.
Инкремент и декремент
Operator ++ (инкремент) и operator – (декремент) предусматривают две версии:
- постфиксную;
- префиксную.
При перегрузке операторов необходимо различать соответствующие формы инкремента и декремента. Для этого у постфиксных версий имеется так называемый фиктивный параметр типа int. При его наличии компилятор будет понимать, что осуществляется перегрузка постфиксной версии. Если фиктивный параметр отсутствует, целесообразно говорить о работе с префиксной версией.
Operator префиксной версии будут возвращать объект после того, как он был увеличен или уменьшен. В постфиксной интерпретации это происходит до уменьшения/увеличения. Из-за данной особенности в необходимо использовать в постфиксной версии временный объект. Он отвечает за возврат значения до его корректировки.
Соответствующее явление также приводит к тому, что при возврате невозможно использовать ссылки. Это вызвано уничтожением временного объекта при осуществлении выхода из функции. В постфиксной версии возвращаемое значение – это объект класса. Он дает меньшую эффективность.
Индексация
Еще один вариант переопределения функции в C++ – это работа с индексацией. Ниже приведен пример реализации соответствующей операции:
Для константных объектов, когда нельзя корректировать их содержимое, допустимо использовать константную версию функции.
При перегрузке индексации допустимо использование проверки передаваемого индекса на факт корректности. В качестве индекса может выступать целое число или любой другой тип данных: string, double и так далее.
Operator ()
Operator () в C++ используется не для изменения объекта, а для его дальнейшего использования в качестве функции. Этот элемент разработки не имеет ограничений на параметры. Подразумевается их количество и типы. Перегрузка оператора осуществляется в качестве метода класса.
Типичным примером объявления может служить запись типа: Foo X::operator() (Bar br, Baz const& bz).
Аллокация и деаллокация
Operators new new[] delete delete[] тоже поддерживают возможность переопределения. Они способны принимать любое количество аргументов. Операторы new new[] в качестве первого аргумента принимают аргумент типа sgt::size_t, а возвращать значение типа void *. Operators delete delete[] принимают первым void * и ничего не возвращать. Соответствующие операции могут быть перегружены как функции, а также для определенных классов.
Выше можно увидеть наглядный пример реализации соответствующей операции.
Пользовательские литералы
После появления C++ 11 версии язык получил так называемые пользовательские литералы. Такие литералы будут вести себя как обычные функции. Они могут обладать квалификатором inline или constexpr. Рекомендуется начинать литерал с символа нижнего подчеркивания. Это необходимо для того, чтобы исключить коллизию с будущими стандартами.
Пользовательские литералы при перегрузке операторов могут принимать только один тип:
- const char *;
- unsigned long long int;
- wchar_t;
- char;
- long double;
- char16_t;
- char32_t.
Достаточно осуществить перегрузку литерала только для типа const char *. Если подходящего кандидата система не нашла, будет произведен вызов оператора с соответствующим типом. Вот наглядный пример преобразования миль в километры при помощи изучаемого процесса:
При перегрузке операторов строковые литералы принимают вторым аргументом std::size_t, а первым, один из:
- const char32_t;
- const wchar_t;
- const char16_t;
- const char *.
Строковые литералы применяются к записям, которые оформлены двойными кавычками.
В C++ имеется встроенный префиксный строковый литерал R. Он воспринимает все символы, написанные в кавычках, как обычные. R не интерпретирует отдельно взятые последовательности в качестве специальных символов. В качестве примера можно взять команду std::cout << R’’(Hello!\n)’’. Она выведет на экран запись Hello!\n.
Чтобы лучше знать C++, рекомендуется пройти дистанционные компьютерные курсы.