Перегрузка операторовПерегрузка операторов в программировании — один из способов реализации полиморфизма, заключающийся в возможности одновременного существования в одной области видимости нескольких различных вариантов применения операторов, имеющих одно и то же имя, но различающихся типами параметров, к которым они применяются. ТерминологияТермин «перегрузка» — это калька английского слова overloading. Такой перевод появился в книгах по языкам программирования в первой половине 1990-х годов. В изданиях советского периода аналогичные механизмы назывались переопределением или повторным определением, перекрытием операций. Причины появленияИногда возникает потребность описывать и применять к созданным программистом типам данных операции, по смыслу эквивалентные уже имеющимся в языке. Классический пример — библиотека для работы с комплексными числами. Они, как и обычные числовые типы, поддерживают арифметические операции, и естественным было бы создать для данного типа операции «плюс», «минус», «умножить», «разделить», обозначив их теми же самыми знаками операций, что и для других числовых типов. Запрет на использование определённых в языке элементов вынуждает создавать множество функций с именами вида ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat и так далее. Когда одинаковые по смыслу операции применяются к операндам различных типов, их вынужденно приходится называть по-разному. Невозможность применять для разных типов функции с одним именем приводит к необходимости выдумывать различные имена для одного и того же, что создаёт путаницу, а может и приводить к ошибкам. Например, в классическом языке Си существует два варианта стандартной библиотечной функции нахождения модуля числа: abs() и fabs() — первый предназначен для целого аргумента, второй — для вещественного. Такое положение, в сочетании со слабым контролем типов Си, может привести к труднообнаруживаемой ошибке: если программист напишет в вычислении abs(x), где x — вещественная переменная, то некоторые компиляторы без предупреждений сгенерируют код, который будет преобразовывать x к целому путём отбрасывания дробной части и вычислять модуль от полученного целого числа. Отчасти проблема решается средствами объектного программирования — когда новые типы данных объявляются как классы, операции над ними могут быть оформлены как методы классов, в том числе и одноимённые (поскольку методы разных классов не обязаны иметь различные имена), но, во-первых, оформление подобным образом операций над значениями разных типов неудобно, а во-вторых, это не решает проблему создания новых операторов. Средства, позволяющие расширять язык, дополнять его новыми операциями и синтаксическими конструкциями (а перегрузка операций является одним из таких средств, наряду с объектами, макрокомандами, функционалами, замыканиями) превращают его уже в метаязык — средство описания языков, ориентированных на конкретные задачи. С его помощью можно для каждой конкретной задачи построить языковое расширение, наиболее ей соответствующее, которое позволит описывать её решение в наиболее естественной, понятной и простой форме. Например, в приложении к перегрузке операций: создание библиотеки сложных математических типов (векторы, матрицы) и описание операций с ними в естественной, «математической» форме, создаёт «язык для векторных операций», в котором сложность вычислений скрыта, и возможно описывать решение задач в терминах векторных и матричных операций, концентрируясь на сути задачи, а не на технике. Именно из этих соображений подобные средства были в своё время включены в язык Алгол-68. Механизм перегрузкиРеализацияПерегрузка операций предполагает введение в язык двух взаимосвязанных особенностей: возможности объявлять в одной области видимости несколько процедур или функций с одинаковыми именами и возможности описывать собственные реализации бинарных операторов (то есть знаков операций, обычно записываемых в инфиксной нотации, между операндами). Принципиально реализация их достаточно проста:
Перегрузка операторов в C++В C++ можно выделить четыре типа перегрузок операторов:
Обычные операторыВажно помнить, что перегрузка расширяет возможности языка, а не изменяет язык, поэтому перегружать операторы для встроенных типов нельзя. Нельзя менять приоритет и ассоциативность (слева направо или справа налево) операторов. Нельзя создавать собственные операторы и перегружать некоторые встроенные: Операторы могут быть перегружены и как отдельные функции, и как функции-члены класса. Во втором случае левым аргументом оператора всегда выступает объект *this. Операторы Можно сильно облегчить написание кода, если производить перегрузку операторов в определённом порядке. Это не только ускорит написание, но и избавит от дублирования одного и того же кода. Рассмотрим перегрузку на примере класса, представляющего собой геометрическую точку в двумерном векторном пространстве: class Point
{
int x, y;
public:
Point(int x, int xx): x(x), y(xx) {} // Конструктор по-умолчанию исчез.
// Имена аргументов конструктора могут совпадать с именами полей класса.
}
Point Point::operator+() {
return Point(*this);
}
Point Point::operator-() {
Point tmp(*this);
tmp.x *= -1;
tmp.y *= -1;
return tmp;
}
bool Point::operator==(const Point& rhs) const {
return (this->x == rhs.x && this->y == rhs.y);
}
bool Point::operator!=(const Point& rhs) const {
return !(*this == rhs);
}
Point& Point::operator++() { //префиксный
x++;
y++;
return *this;
}
Point Point::operator++(int) { //постфиксный
Point tmp(x,y,i);
++(*this);
return tmp;
}
На остальные операторы не распространяются какие-либо общие рекомендации к перегрузке. Преобразования типовПреобразования типов позволяют задать правила преобразования нашего класса к другим типам и классам. Также можно указать спецификатор explicit, который позволит преобразовывать типы только, если программист явно это указал (например static_cast<Point3>(Point(2,3));). Пример: Point::operator bool() const {
return this->x != 0 || this->y != 0;
}
Операторы аллокации и деаллокации
Операторы Пример: void* MyClass::operator new(std::size_t s, int a)
{
void * p = malloc(s*a);
if(p == nullptr)
throw "No free memory!";
return p;
}
// ...
// Вызов:
MyClass * p = new(12) MyClass;
Пользовательские литералыПользовательские литералы появились с одиннадцатого стандарта C++. Литералы ведут себя как обычные функции. Они могут быть с квалификатором inline или constexpr. Желательно, чтобы литерал начинался с символа нижнего подчёркивания, так как может возникнуть коллизия с будущими стандартами. Например, литерал i уже принадлежит комплексным числам из Литералы могут принимать только один из следующих типов: constexpr int operator "" _mi (unsigned long long int i)
{ return 1.6 * i;}
constexpr double operator "" _mi (long double i)
{ return 1.6 * i;}
Строковые литералы принимают вторым аргументом В C++ встроен префиксный строковый литерал R, который воспринимает все символы в кавычках как обычные и не интерпретирует определённые последовательности в специальные символы. Например, такая команда Пример реализации на С#Перегрузка операторов тесно связана с перегрузкой методов. Для перегрузки оператора служит ключевое слово Operator, определяющее «операторный метод», который, в свою очередь, определяет действие оператора относительно своего класса. Существует две формы операторных методов (operator): одна — для унарных операторов, другая для бинарных. Ниже приведена общая форма для каждой разновидности этих методов. // общая форма перегрузки унарного оператора.
public static возвращаемый_тип operator op (тип_параметра операнд)
{
// операции
}
// Общая форма перегрузки бинарного оператора.
public static возвращаемый_тип operator op (тип_параметра1 операнд1,
тип_параметра2 операнд2)
{
// операции
}
Здесь вместо «op» подставляется перегружаемый оператор, например + или /; а «возвращаемый_тип» обозначает конкретный тип значения, возвращаемого указанной операцией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упрощает применение перегружаемых операторов в выражениях. Для унарных операторов операнд обозначает передаваемый операнд, а для бинарных операторов то же самое обозначают «операнд1 и операнд2». Следует обратить внимание, что операторные методы должны иметь оба типа, public и static. Тип операнда унарных операторов должен быть таким же, как и у класса, для которого перегружается оператор. А в бинарных операторах хотя бы один из операндов должен быть такого же типа, как и у его класса. Следовательно, в C# не допускается перегрузка любых операторов для объектов, которые еще не были созданы. Например, назначение оператора + нельзя переопределить для элементов типа int или string. В параметрах оператора нельзя использовать модификатор ref или out.[1] Варианты и проблемыПерегрузка процедур и функций на уровне общей идеи, как правило, не представляет сложности ни в реализации, ни в понимании. Однако даже в ней имеются некоторые «подводные камни», которые необходимо учитывать. Разрешение перегрузки операций создаёт гораздо больше проблем как для реализатора языка, так и для работающего на этом языке программиста. Проблема идентификацииПроблема первая — контекстная зависимость. Т. е. первый вопрос с которым сталкивается разработчик транслятора языка, разрешающего перегрузку процедур и функций: каким образом из числа одноимённых процедур выбрать ту, которая должна быть применена в данном конкретном случае? Всё хорошо, если существует вариант процедуры, типы формальных параметров которого в точности совпадают с типами параметров фактических, применённых в данном вызове. Однако практически во всех языках в употреблении типов существует некоторая степень свободы, предполагающая, что компилятор в определённых ситуациях автоматически безопасно преобразовывает (приводит) типы данных. Например, в арифметических операциях над вещественным и целочисленными аргументами целочисленный обычно приводится к вещественному типу автоматически, и результат получается вещественным. Предположим, что существует два варианта функции add: int add(int a1, int a2); float add(float a1, float a2); Каким образом компилятор должен обработать выражение Возникает вопрос: должен ли транслятор разрешать подобное использование перегруженных функций, а если должен, то на каком основании он будет выбирать конкретный используемый вариант? В частности, в приведённом выше примере, должен ли транслятор при выборе учитывать тип переменной y? Нужно отметить, что приведённая ситуация — простейшая. Но возможны гораздо более запутанные случаи, которые усугубляются тем, что не только встроенные типы могут преобразовываться по правилам языка, но и объявленные программистом классы при наличии у них родственных отношений допускают приведение одного к другому. Решений у этой проблемы два:
Специфические вопросы перегрузки операцийВ отличие от процедур и функций, инфиксные операции языков программирования имеют два дополнительных свойства, существенным образом влияющие на их функциональность: приоритет и ассоциативность, наличие которых обусловливается возможностью «цепочной» записи операторов (как понимать Встроенные в язык операции всегда имеют наперёд заданные традиционные приоритеты и ассоциативность. Возникает вопрос: какие приоритеты и ассоциативность будут иметь переопределённые версии этих операций или, тем более, новые созданные программистом операции? Есть и другие тонкости, которые могут требовать уточнения. Например, в Си существуют две формы операций увеличения и уменьшения значения Различные языки по-разному решают приведённые вопросы. Так, в C++ приоритет и ассоциативность перегруженных версий операций сохраняются такими же, как и у предопределённых в языке, а описания перегрузки префиксной и постфиксной формы операторов инкремента и декремента используют различные сигнатуры:
Фактически целого параметра у операции нет — он фиктивен, и добавляется только для внесения различия в сигнатуры Ещё один вопрос: допускать ли возможность перегрузки операций для встроенных и для уже объявленных типов данных? Может ли программист изменить реализацию операции сложения для встроенного целочисленного типа? Или для библиотечного типа «матрица»? Как правило, на первый вопрос отвечают отрицательно. Изменение поведения стандартных операций для встроенных типов — чрезвычайно специфическое действие, реальная необходимость в котором может возникать лишь в редких случаях, тогда как вредные последствия бесконтрольного применения такой возможности трудно даже предугадать во всей полноте. Поэтому язык обычно либо запрещает переопределять операции для встроенных типов, либо реализует механизм перегрузки операторов таким образом, чтобы с его помощью стандартные операции просто невозможно было бы перекрыть. Что касается второго вопроса (переопределение операторов, уже описанных для существующих типов), то необходимая функциональность полностью обеспечивается механизмом наследования классов и переопределения методов: если требуется изменить поведение уже имеющегося класса, его нужно унаследовать и переопределить описанные в нём операторы. При этом старый класс останется без изменений, новый получит нужную функциональность, а никаких коллизий не возникнет. Объявление новых операцийЕщё сложнее обстоит дело с объявлением новых операций. Включить в язык саму возможность такого объявления несложно, но вот реализация его сопряжена со значительными трудностями. Объявление новой операции — это, фактически, создание нового ключевого слова языка программирования, осложнённое тем фактом, что операции в тексте, как правило, могут следовать без разделителей с другими лексемами. При их появлении возникают дополнительные трудности в организации лексического анализатора. Например, если в языке уже есть операции «+» и унарный «-» (изменение знака), то выражение Не следует забывать, что для новых операций также стоит вопрос определения ассоциативности и приоритета. Здесь уже нет готового решения в виде стандартной языковой операции, и обычно приходится просто задать эти параметры правилами языка. Например, сделать все новые операции левоассоциативными и дать им один и тот же, фиксированный, приоритет, либо ввести в язык средства задания того и другого. Перегрузка и полиморфные переменныеКогда перегружаемые операции, функции и процедуры используются в языках со строгой типизацией, где каждая переменная имеет предварительно описанный тип, задача выбора варианта перегруженной операции, используемого в каждом конкретном случае, независимо от её сложности, решается транслятором. Это означает, что для компилируемых языков использование перегрузки операций никак не снижает быстродействие, — в любом случае, в объектном коде программы присутствует вполне определённая операция или вызов функции. Иначе обстоит дело при возможности использования в языке полиморфных переменных — переменных, могущих в разные моменты времени содержать значения разных типов. Так как тип значения, к которому будет применяться перегруженная операция, неизвестен на момент трансляции кода, то компилятор лишается возможности выбрать нужный вариант заранее. В этой ситуации он вынужден встраивать в объектный код фрагмент, который непосредственно перед выполнением данной операции определит типы находящихся в аргументах значений и динамически выберет вариант, соответствующий этому набору типов. Причём такое определение нужно производить при каждом выполнении операции, — ведь даже тот же самый код, будучи вызван второй раз, вполне может исполняться по-другому… Таким образом, использование перегрузки операций в сочетании с полиморфными переменными делает неизбежным динамическое определение вызываемого кода. КритикаИспользование перегрузки не всеми специалистами считается благом. Если перегрузка функций и процедур, в общем, не находит серьёзных возражений (отчасти, потому, что не приводит к некоторым типично «операторным» проблемам, отчасти — из-за меньшего соблазна её использования не по назначению), то перегрузка операций, как в принципе, так и в конкретных языковых реализациях, подвергается достаточно жёсткой критике со стороны многих теоретиков и практиков программирования. Критики отмечают, что приведённые выше проблемы идентификации, приоритета и ассоциативности часто делают работу с перегруженными операциями либо неоправданно сложной, либо неестественной:
Насколько удобство от использования собственных операций способно перевесить неудобства от ухудшения управляемости программы — вопрос, не имеющий однозначного ответа. Часть критиков высказываются против перегрузки операций, исходя из общих принципов теории разработки программного обеспечения и реальной промышленной практики.
Данная проблема естественным образом вытекает из двух предыдущих. Она легко нивелируется принятием соглашений и общей культурой программирования. КлассификацияНиже приведена классификация некоторых языков программирования по тому, допускают ли они перегрузку операторов, и ограничены ли операторы предопределённым набором:
Примечания
См. также
|
Portal di Ensiklopedia Dunia