Перегрузка
Если точного соответствия ни на одном уровне не найдено, но найдено несколько подходящих функций на разных уровнях, то используется функция, найденная на наименьшем уровне. В пределах одного уровня не может быть более одной подходящей функции.
- Инкапсуляция и расширяемость типов
- Наследование
- Полиморфизм
- Перегрузка
- Виртуальные функции
- Статические члены класса
- Шаблоны функций
- Шаблоны классов
- Абстрактные классы
Присоединяйся к нам — скачай MetaTrader 5!
Перегрузка []
Кроме нескольких вышеперечисленных операторов, можно перегрузить и любые другие операторы С++. Большей частью требуется перегружать стандартные операторы, такие как арифметические, логические или операторы отношения. Тем не менее имеется один достаточно экзотичный оператор, который бывает полезно перегружать: []. В С++ оператор [] при перегрузке рассматривается как бинарный оператор. Его следует перегружать с помощью функции-члена. Нельзя использовать дружественную функцию. Общая форма функции-оператора operator[]() имеет следующий вид:
Параметр не обязан иметь тип int, но поскольку функция operator[]() обычно используется для индексации массива, то в таком качестве обычно используется целое значение.
Для заданного объекта О выражение
преобразуется в вызов функции operator[]():
В таком случае значение индекса передается функции operator[]() в качестве явного параметра. Указатель this указывает на объект О, тот самый, который вызывает функцию.
В следующей программе класс atype содержит массив из трех переменных целого типа. Конструктор инициализирует каждый элемент массива заданным значением. Перегруженная функция-оператор operator[]() возвращает величину элемента массива, определяемого индексом, передаваемым в качестве параметра.
#include
class atype int a[3];
public:
atype (int i, int j, int k) a[0] = i;
a[1] = j;
a[2] = k;
>
int operator[] (int i) < return a[i]; >
>;
int main()
atype ob(1, 2, 3);
cout return 0;
>
Можно создать функцию-оператор operator[]() таким образом, чтобы оператор [] можно было использовать как с левой, так и с правой стороны оператора присваивания. Для этого достаточно в качестве возвращаемой величины для operator[]() задать ссылку. Сказанное проиллюстрировано в следующей программе:
Поскольку operator[]() возвращает ссылку на элемент массива, отвечающий индексу i, то он может быть использован с левой стороны операции присваивания для модификации элемента массива. Разумеется, этот оператор может быть использован и с правой стороны оператора присваивания.
Как известно, в С++ во время выполнения программы можно выйти за пределы массива, и при этом не будет выдаваться сообщение об ошибке. Одним из достоинств перегрузки оператора [] служит то, что с его помощью можно предотвратить подобные эффекты.
Если создается класс, содержащий массив, и разрешен доступ к этому массиву только через перегруженный оператор [], то можно перехватывать значение, содержащее величину индекса за пределами допустимых значений. Например, следующая программа осуществляет проверку на принадлежность значений допустимой области:
// пример безопасного массива
#include
#include
class atype int a[3];
public:
atype (int i, int j, int k) a[0] = i;
a[1] = j;
a[2] = k;
>
int &operator[] (int i);
>;
// проверка диапазона для atype.
int &atype::operator [] (int i)
if (i2) cout exit (1);
>
return a[i];
>
int main()
atype ob(1, 2, 3);
cout cout ob[1] = 25; // [] слева
cout ob[3] = 44; // генерируется ошибка времени выполнения, поскольку 3 выходит за допустимые пределы
return 0;
>
При выполнении инструкции
оператор перехватывает ошибку выхода за допустимую область и программа заканчивает свою работу прежде, чем сможет нанести ущерб системе.
Что такое перегрузка в программировании
Перегрузка операторов позволяет использовать для абстрактных типов данных привычные операторы, например + для сложения или объединения двух объектов, = для присвоения одного экземпляра объекта другому и т.д. Комплексная арифметика, матричная алгебра, символьные строки — это лишь немногие примеры, где удобно использовать перегруженные операторы.
Например, для комплексных чисел (типа данных Complex) можно перегрузить следующие операторы:
- * – комплексное умножение
- + – комплексное сложение
- ~ – комплексное сопряжение
Complex x,y,z; z=x*y; z=x.operator*(y); // явный вызов оператора class Complex < … Complex operator*(const Complex& other) const; … >
Исходя из этой записи, нетрудно догадаться, как осуществляется перегрузка операторов для абстрактных типов данных. Просто нужно определить функцию-член с именем operator* или любой другой оператор.
Рассмотрим пример класса Complex, а затем разберем подробно перегрузку некоторых типовых операторов.
6.2.1. Пример 6.1 (класс Complex (комплексное число))
///////////////////////////////////////////////////////////////////////////// // Прикладное программирование // Пример 6.1. Класс Комплексное число // complex.h // // Кафедра Прикладной и компьютерной оптики, http://aco.ifmo.ru // Университет ИТМО ///////////////////////////////////////////////////////////////////////////// // проверка на повторное подключение файла #if !defined COMPLEX_H #define COMPLEX_H #include using namespace std; ///////////////////////////////////////////////////////////////////////////// // класс Комплексное число class Complex < private: // вещественная и мнимая часть комплексного числа double m_re, m_im; public: // конструкторы Complex(); Complex(double re, double im=0); Complex(const Complex& other); // получение параметров комплексного числа double GetRe() const; double GetIm() const; // изменение параметров комплексного числа void Set(double re, double im=0.); // оператор умножения Complex operator*(const Complex& other) const; // оператор умножения на число Complex operator*(const double& other) const; // оператор умножения с присваиванием Complex& operator*=(const Complex& other); // оператор присваивания Complex& operator=(const Complex& other); // оператор равенства bool operator== (const Complex& other) const; // оператор сопряжения комплексного числа Complex operator~() const; // унарный минус Complex operator-() const; // ввод/вывод комплексного числа friend ostream& operatorconst Complex& x); friend istream& operator>> (istream& out, Complex& x); // преобразование типа Complex в double operator double() const; >; ///////////////////////////////////////////////////////////////////////////// // получение вещественной части комплексного числа inline double Complex::GetRe() const < return m_re; > ///////////////////////////////////////////////////////////////////////////// // получение мнимой части комплексного числа inline double Complex::GetIm() const < return m_im; > ///////////////////////////////////////////////////////////////////////////// // изменение параметров комплексного числа inline void Complex::Set(double re, double im) < m_re=re; m_im=im; >///////////////////////////////////////////////////////////////////////////// #endif //defined COMPLEX_H
///////////////////////////////////////////////////////////////////////////// // Прикладное программирование // Пример 6.1. Класс Комплексное число // complex.cpp // // Кафедра Прикладной и компьютерной оптики, http://aco.ifmo.ru // Университет ИТМО ///////////////////////////////////////////////////////////////////////////// // подключение описания класса #include "complex.h" ///////////////////////////////////////////////////////////////////////////// // конструктор по умолчанию Complex::Complex() : m_re(0.) , m_im(0.) < >///////////////////////////////////////////////////////////////////////////// // полный конструктор Complex::Complex(double re, double im) : m_re(re) , m_im(im) < >///////////////////////////////////////////////////////////////////////////// // конструктор копирования Complex::Complex(const Complex& x) : m_re(x.m_re) , m_im(x.m_im) < >///////////////////////////////////////////////////////////////////////////// // оператор умножения Complex Complex::operator*(const Complex& other) const < return Complex(m_re*other.m_re-m_im*other.m_im, m_re*other.m_im-m_im*other.m_re); > ///////////////////////////////////////////////////////////////////////////// // оператор умножения на число Complex Complex::operator*(const double& other) const < return Complex(m_re*other, m_im*other); > ///////////////////////////////////////////////////////////////////////////// // оператор умножения с присваиванием Complex& Complex::operator*=(const Complex& other) < Complex temp(*this); m_re=temp.m_re*other.m_re - temp.m_im*other.m_im; m_im=temp.m_re*other.m_im + temp.m_im*other.m_re; return (*this); > ///////////////////////////////////////////////////////////////////////////// // унарный минус Complex Complex::operator-() const < return Complex(-m_re, -m_im); > ///////////////////////////////////////////////////////////////////////////// // оператор сопряжения комплексного числа Complex Complex::operator~() const < return Complex(m_re, -m_im); > ///////////////////////////////////////////////////////////////////////////// // оператор равенства bool Complex::operator== (const Complex& other) const < return (m_re == other.m_re && m_im == other.m_im); > ///////////////////////////////////////////////////////////////////////////// // оператор присваивания Complex& Complex::operator=(const Complex& other) < if(this != &other) < m_re=other.m_re; m_im=other.m_im; >return *this; > ///////////////////////////////////////////////////////////////////////////// // преобразование типа Complex в double Complex::operator double() const < return (m_re*m_re-m_im*m_im); > ///////////////////////////////////////////////////////////////////////////// // вывод комплексного числа на экран ostream& operatorconst Complex& x) < return (out"("<< ); > ///////////////////////////////////////////////////////////////////////////// // ввод комплексного числа с клавиатуры istream& operator>> (istream& in, Complex& x) < return (in>>x.m_re>>x.m_im); > /////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////// // Прикладное программирование // Пример 6.1. Класс Комплексное число // test_complex.cpp // // Кафедра Прикладной и компьютерной оптики, http://aco.ifmo.ru // Университет ИТМО ///////////////////////////////////////////////////////////////////////////// #include using namespace std; // подключение описания класса #include "complex.h" ///////////////////////////////////////////////////////////////////////////// // пример использования класса Complex void main() < Complex x(1,1), y(2,2), res1, res2, res3; Complex x1; // тестирование преобразования типов // преобразование вещественного числа в комплексное при помощи конструктора res1=Complex(3.14); cout"Complex(3.14): "// преобразование комплекcного числа в вещественное при помощи перегруженного оператора double double c=double(res1); cout"double(res1): "// тестирование арифметических операторов res1=x*y; // перемножение двух комплексных чисел res1=x.operator*(y); // то же самое, явный вызов оператора res2=-x; // унарный минус res3=~x; // комлексное сопряжение x*=y; // умножение с присваиванием cout" x*y="<< < // тестирование оператора равенства if(x==y) cout< else cout< // тестирование операторов присваивания res3=res2=res1=1; cout< < /////////////////////////////////////////////////////////////////////////////
6.2.2. Перегрузка бинарных операторов
// оператор умножения Complex Complex::operator*(const Complex& other) const < return Complex(m_re*other.m_re-m_im*other.m_im, m_re*other.m_im-m_im*other.m_re); > // оператор умножения на число Complex Complex::operator*(const double& other) const < return Complex(m_re*other, m_im*other); > // пример использования Complex x, z; double y; z=x*y;
6.2.3. Перегрузка унарных операторов
// унарный минус Complex Complex::operator-() const < return Complex(-m_re, -m_im); > // оператор сопряжения комплексного числа Complex Complex::operator~() const < return Complex(m_re, -m_im); > // пример использования Complex x, y; y=-x; y=~x;
6.2.4. Перегрузка логических операторов
// оператор равенства bool Complex::operator== (const Complex& other) const < return (m_re == other.m_re && m_im == other.m_im); > // пример использования Complex x, y; if(x==y)
6.2.5. Перегрузка оператора присваивания
Перегрузку оператора присваивания следует выделить особо. Мы уже рассматривали функцию, которая позволяет инициализировать один экземпляр класса значениями переменных членов хранящихся в другом экземпляре. Это конструктор копирования. Оператор присваивания отличается тем, что новый класс при его выполнении не создаётся, а значения членов одного экземпляра присваиваются членам другого экземпляра. При перегрузке оператора присваивания необходимо руководствоваться следующими правилами:
- Аргументом перегруженного оператора присваивания должна быть неизменяемая ссылка на экземпляр данного класса (чтобы случайно не испортить экземпляр).
- Перед осуществлением присваивания необходимо осуществить проверку на присваивание самому себе (чтобы не выполнять лишних действий).
- Оператор должен осуществлять поэлементное присваивание (то есть должно быть выполнено последовательное присваивание каждой переменной).
- Оператор должен возвращать ссылку на самого себя (чтобы оно было похоже на присваивание встроенных типов данных, тогда будет возможна запись x=y=z=1 ).
// оператор присваивания Complex& Complex::operator=(const Complex& other) < if(this != &other) < m_re=other.m_re; m_im=other.m_im; >return *this; > // пример использования Complex x, y, z; x=y=z=1;
6.2.6. Перегрузка операторов с присваиванием
Перегрузка операторов с присваиванием (+=, *= и т.д.) осуществляется по следующим правилам:
- Аргументом перегруженного оператора с присваиванием должна быть неизменяемая ссылка на экземпляр данного класса (чтобы случайно не испортить экземпляр).
- Перегруженный оператор с присваиванием должен возвращать ссылку на самого себя.
// оператор умножения с присваиванием Complex& Complex::operator*=(const Complex& other) < Complex temp(*this); m_re=temp.m_re*other.m_re - temp.m_im*other.m_im; m_im=temp.m_re*other.m_im + temp.m_im*other.m_re; return (*this); > // пример использования Complex x, y; x*=y;
6.2.7. Перегрузка преобразования типов
Преобразовать встроенный тип данных к нашему абстрактному типу можно при помощи конструктора (см. раздел 6.1.2). А чтобы преобразовать абстрактный тип данных к встроенному типу, необходимо перегрузить оператор:
// преобразование типа Complex в double Complex::operator double() const < return (m_re*m_re-m_im*m_im); > // пример использования Complex x; double y; y=double(x);
6.2.8. Перегрузка оператора доступа по индексу
Для некоторых классов, которые хранят массивы (например, матрица) для удобного доступа к элементу массива по его индексу можно перегрузить оператор ():
// оператор доступа по индексу double& matrix::operator() (int i, int j) < return (p[i][j]); // или p[i*size+j]; > // пример использования matrix x; double y; y=matrix(1,1); // доступ к элементу (1,1)
6.2.9. Перегрузка операторов ввода/вывода
Перегрузку операторов ввода/вывода приходится оформлять в виде дружественных функций класса.
Это происходит из-за того, что при использовании оператора ввода или вывода слева от него должен находиться экземпляр потока ввода/вывода, то есть перегруженные операторы ввода/вывода являются членами потоков, а не других классов. Вмешаться во внутреннюю реализацию потоков мы не можем, но можем использовать функции, которые не являются членами классов, но позволяют получить доступ к их скрытым (private) членам.
// oписание: friend ostream& operatorconst Complex& x); friend istream& operator>> (istream& out, Complex& x); // вывод комплексного числа на экран ostream& operatorconst Complex& x) < return (out"("<< ); > // ввод комплексного числа с клавиатуры istream& operator>> (istream& in, Complex& x) < return (in>>x.m_re>>x.m_im); > // пример использования Complex x; cout 6.2.10. Неперегружаемые операторы
Не могут быть перегружены следующие операторы:
- Оператор:: (левый и правый операнд являются не значениями, а именем)
- Оператор. (правый операнд является именем)
- Оператор.* (правый операнд является именем)
- Оператор? : (арифметический оператор имеет специфическую семантику)
- Оператор new (операнд является именем, кроме того выполняет небезопасную процедуру)
- Оператор delete (не используется без new, кроме того выполняет небезопасную процедуру)
Кроме того, с помощью механизма перегрузки можно переопределить только существующие операторы. Новые операторы определить невозможно.
Перегрузка операций
Перегрузка операций (операторов, функций, процедур) — в программировании — один из способов реализации полиморфизма, заключающийся в возможности одновременного существования в одной области видимости нескольких различных вариантов операции (оператора, функции или процедуры), имеющих одно и то же имя, но различающихся типами параметров, к которым они применяются.
Терминология
Термин «перегрузка» — это калька английского «overloading», появившаяся в русских переводах книг по языкам программирования в первой половине 1990-х годов. Возможно, это не самый лучший вариант перевода, поскольку слово «перегрузка» в русском языке имеет устоявшееся собственное значение, кардинально отличающееся от вновь предложенного, тем не менее, он прижился и распространён достаточно широко. В изданиях советского времени аналогичные механизмы назывались по-русски «переопределением» или «повторным определением» операций, но и этот вариант небесспорен: возникают разночтения и путаница в переводах английских «override», «overload» и «redefine».
Причины появления
В большинстве ранних языков программирования существовало ограничение, согласно которому одновременно в программе не может быть доступно более одной операции с одним и тем же именем. Соответственно, все функции и процедуры, видимые в данной точке программы, должны иметь различные имена. Имена и обозначения функций, процедур и операторов, являющихся частью языка программирования, не могут быть использованы программистом для именования собственных функций, процедур и операторов. В некоторых случаях программист может создать собственный программный объект с именем другого, уже имеющегося, но тогда вновь созданный объект «перекрывает» предыдущий, и одновременно использовать оба варианта становится невозможно.
Подобное положение неудобно в некоторых, достаточно часто встречающихся случаях.
- Иногда возникает потребность описывать и применять к созданным программистом типам данных операции, по смыслу эквивалентные уже имеющимся в языке. Классический пример — библиотека для работы с комплексными числами. Они, как и обычные числовые типы, поддерживают арифметические операции, и естественным было бы создать для данного типа операции «плюс», «минус», «умножить», «разделить», обозначив их теми же самыми знаками операций, что и для других числовых типов. Запрет на использование определённых в языке элементов вынуждает создавать множество функций с именами вида ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat и так далее.
- Когда одинаковые по смыслу операции применяются к операндам различных типов, их вынужденно приходится называть по-разному. Невозможность применять для разных типов функции с одним именем приводит к необходимости выдумывать различные имена для одного и того же, что создаёт путаницу, а может и приводить к ошибкам. Например, в классическом языке Си существует два варианта стандартной библиотечной функции нахождения модуля числа: abs() и fabs() — первый предназначен для целого аргумента, второй — для вещественного. Такое положение, в сочетании со слабым контролем типов Си, может привести к труднообнаруживаемой ошибке: если программист напишет в вычислении abs(x), где x — вещественная переменная, то некоторые компиляторы без предупреждений сгенерируют код, который будет преобразовывать x к целому путём отбрасывания дробной части и вычислять модуль от полученного целого числа!
Отчасти проблема решается средствами объектного программирования — когда новые типы данных объявляются как классы, операции над ними могут быть оформлены как методы классов, в том числе и одноимённые (поскольку методы разных классов не обязаны иметь различные имена), но, во-первых, оформление подобным образом операций над значениями разных типов неудобно, а во-вторых, это не решает проблему создания новых операторов.
Сама по себе перегрузка операций — всего лишь «синтаксический сахар», хотя даже в таком качестве она может быть полезна, потому что она позволяет разработчику программировать более естественным образом и делает поведение пользовательских типов более похожим на поведение встроенных. Если же подойти к вопросу с более общих позиций, то можно заметить, что средства, позволяющие расширять язык, дополнять его новыми операциями и синтаксическими конструкциями (а перегрузка операций является одним из таких средств, наряду с объектами, макрокомандами, функционалами, замыканиями) превращают его уже в метаязык — средство описания языков, ориентированных на конкретные задачи. С его помощью можно для каждой конкретной задачи построить языковое расширение, наиболее ей соответствующее, которое позволит описывать её решение в наиболее естественной, понятной и простой форме. Например, в приложении к перегрузке операций: создание библиотеки сложных математических типов (векторы, матрицы) и описание операций с ними в естественной, «математической» форме, создаёт «язык для векторных операций», в котором сложность вычислений скрыта, и возможно описывать решение задач в терминах векторных и матричных операций, концентрируясь на сути задачи, а не на технике. Именно из этих соображений подобные средства были в своё время включены в язык Алгол-68.
Механизм перегрузки
Реализация
Перегрузка операций предполагает введение в язык двух взаимосвязанных особенностей: возможности объявлять в одной области видимости несколько процедур или функций с одинаковыми именами и возможности описывать собственные реализации операций (то есть знаков операций, обычно записываемых в инфиксной нотации, между операндами). Принципиально реализация их достаточно проста:
- Чтобы разрешить существование нескольких одноимённых операций, достаточно ввести в язык правило, согласно которому операция (процедура, функция или оператор) опознаются компилятором не только по имени (обозначению), но и по типам их параметров. Таким образом, abs(i), где i объявлено как целое, и abs(x), где x объявлено как вещественное — это две разные операции. Принципиально в обеспечении именно такой трактовки нет никаких сложностей.
- Чтобы дать возможность определять и переопределять операции, необходимо ввести в язык соответствующие синтаксические конструкции. Вариантов их может быть достаточно много, но по сути они ничем друг от друга не отличаются, достаточно помнить, что запись вида « » принципиально аналогична вызову функции «(,)». Достаточно разрешить программисту описывать поведение операторов в виде функций — и проблема описания решена.
Варианты и проблемы
Перегрузка процедур и функций на уровне общей идеи, как правило, не представляет сложности ни в реализации, ни в понимании. Однако даже в ней имеются некоторые «подводные камни», которые необходимо учитывать. Разрешение перегрузки операций создаёт гораздо больше проблем как для реализатора языка, так и для работающего на этом языке программиста.
Проблема идентификации
Первый вопрос, с которым сталкивается разработчик транслятора языка, разрешающего перегрузку процедур и функций: каким образом из числа одноимённых процедур выбрать ту, которая должна быть применена в данном конкретном случае? Всё хорошо, если существует вариант процедуры, типы формальных параметров которого в точности совпадают с типами параметров фактических, применённых в данном вызове. Однако практически во всех языках в употреблении типов существует некоторая степень свободы, предполагающая, что компилятор в определённых ситуациях автоматически выполняет безопасные преобразования типов. Например, в арифметических операциях над вещественным и целым аргументами целый обычно приводится к вещественному типу автоматически, и результат получается вещественным. Предположим, что существует два варианта функции add:
int add(int a1, int a2); float add(float a1, float a2);Каким образом компилятор должен обработать выражение y = add(x, i) , где x имеет тип float, а i — тип int? Очевидно, что точного совпадения нет. Имеется два варианта: либо y=add_int((int)x,i) , либо как y=add_flt(x, (float)i) (здесь именами add_int и add_float обозначены соответственно, первый и второй варианты функции).
Возникает вопрос: должен ли транслятор разрешать подобное использование перегруженных функций, а если должен, то на каком основании он будет выбирать конкретный используемый вариант? В частности, в приведённом выше примере, должен ли транслятор при выборе учитывать тип переменной y? Нужно отметить, что приведённая ситуация — простейшая, возможны гораздо более запутанные случаи, которые усугубляются тем, что не только встроенные типы могут преобразовываться по правилам языка, но и объявленные программистом классы при наличии у них родственных отношений допускают приведение один к другому. Решений у этой проблемы два:
- Запретить неточную идентификацию вообще. Требовать, чтобы для каждой конкретной пары типов существовал в точности подходящий вариант перегруженной процедуры или операции. Если такого варианта нет, транслятор должен выдавать ошибку. Программист в этом случае должен применить явное преобразование, чтобы привести фактические параметры к нужному набору типов. Этот подход неудобен в языках типа C++, допускающих достаточную свободу в обращении с типами, поскольку он приводит к существенному различию поведения встроенных и перегруженных операций (к обычным числам арифметические операции можно применять, не задумываясь, а к другим типам — только с явным преобразованием) либо к появлению огромного количества вариантов операций.
- Установить определённые правила выбора «ближайшего подходящего варианта». Обычно в этом варианте компилятор выбирает те из вариантов, вызовы которых можно получить из исходного только безопасными (не приводящими к потере информации) преобразованиями типов, а если их несколько — может выбирать, исходя из того, какой вариант требует меньше таких преобразований. Если в результате остаётся несколько возможностей, компилятор выдаёт ошибку и требует явного указания варианта от программиста.
Специфические вопросы перегрузки операций
В отличие от процедур и функций, инфиксные операции языков программирования имеют два дополнительных свойства, существенным образом влияющих на их функциональность: приоритет и ассоциативность, наличие которых обуславливается возможностью «цепочной» записи операторов (как понимать a+b*c : как (a+b)*c или как a+(b*c) ? Выражение a-b+c — это (a-b)+c или a-(b+c) ?).
Встроенные в язык операции всегда имеют наперёд заданные традиционные приоритеты и ассоциативность. Возникает вопрос: какие приоритеты и ассоциативность будут иметь переопределённые версии этих операций или, тем более, новые созданные программистом операции? Есть и другие тонкости, которые могут требовать уточнения. Например, в Си существуют две формы операций увеличения и уменьшения значения ++ и -- — префиксная и постфиксная, поведение которых различается. Как должны вести себя перегруженные версии таких операций?
Различные языки по-разному решают приведённые вопросы. Так, в C++ приоритет и ассоциативность перегруженных версий операций сохраняются такими же, как и у определённых в языке; перегрузить отдельно префиксную и постфиксную форму операторов инкремента и декреманта возможно, используя специальные сигнатуры:
Префиксная форма Постфиксная форма Функция T &operator ++(T &) T operator ++(T &, int) Функция-член T &T::operator ++() T T::operator ++(int) Таким образом, int используется для внесения различия в сигнатуры
Объявление новых операций
Ещё сложнее обстоит дело с объявлением новых операций. Включить в язык саму возможность такого объявления несложно, но вот реализация его сопряжена со значительными трудностями. Объявление новой операции — это, фактически, создание нового ключевого слова языка программирования, осложнённое тем фактом, что операции в тексте, как правило, могут следовать без разделителей с другими лексемами. При их появлении возникают дополнительные трудности в организации лексического анализатора. Например, если в языке уже есть операции «+» и унарный «-» (изменение знака), то выражение a+-b можно безошибочно трактовать как a + (-b) , но если в программе объявляется новая операция +- , тут же возникает неоднозначность, ведь то же выражение можно уже разобрать и как a (+-) b . Разработчик и реализатор языка должен каким-то образом решать подобные проблемы. Варианты, опять-таки, могут быть различными: потребовать, чтобы все новые операции были односимвольными, постулировать, что при любых разночтениях выбирается «самый длинный» вариант операции (то есть до тех пор, пока очередной читаемый транслятором набор символов совпадает с какой-либо операцией, он продолжает считываться), пытаться обнаруживать коллизии при трансляции и выдавать ошибки в спорных случаях… Так или иначе, языки, допускающие объявление новых операций, решают эти проблемы.
Не следует забывать, что для новых операций также стоит вопрос определения ассоциативности и приоритета. Здесь уже нет готового решения в виде стандартной языковой операции, и обычно приходится просто задать эти параметры правилами языка. Например, сделать все новые операции левоассоциативными и дать им один и тот же, фиксированный, приоритет, либо ввести в язык средства задания того и другого.
Перегрузка и полиморфные переменные
Когда перегружаемые операции, функции и процедуры используются в языках со строгой типизацией, где каждая переменная имеет предварительно описанный тип, задача выбора варианта перегруженной операции, используемого в каждом конкретном случае, независимо от её сложности, решается транслятором. Это означает, что для компилируемых языков использование перегрузки операций не приводит к снижению быстродействия — в любом случае, в объектном коде программы присутствует вполне определённая операция или вызов функции. Иначе обстоит дело при возможности использования в языке полиморфных переменных, то есть переменных, которые могут в разные моменты времени содержать значения разных типов.
Поскольку тип значения, к которому будет применяться перегруженная операция, неизвестен на момент трансляции кода, компилятор лишён возможности выбрать нужный вариант заранее. В этом случае он вынужден встраивать в объектный код фрагмент, который непосредственно перед выполнением данной операции определит типы находящихся в аргументах значений и динамически выберет вариант, соответствующий этому набору типов. Причём такое определение нужно производить при каждом исполнении операции, ведь даже тот же самый код, будучи вызван второй раз, вполне может исполняться по-другому.
Таким образом, использование перегрузки операций в сочетании с полиморфными переменными делает неизбежным динамическое определение вызываемого кода.
Критика
Использование перегрузки не всеми специалистами считается благом. Если перегрузка функций и процедур, в общем, не находит серьёзных возражений (отчасти, потому, что не приводит к некоторым типично «операторным» проблемам, отчасти — из-за меньшего соблазна её использования не по назначению), то перегрузка операций, как в принципе, так и в конкретных языковых реализациях, подвергается достаточно жёсткой критике со стороны многих теоретиков и практиков программирования.
Критики отмечают, что приведённые выше проблемы идентификации, приоритета и ассоциативности часто делают работу с перегруженными операциями либо неоправданно сложной, либо неестественной:
- Идентификация. Если в языке приняты жёсткие правила идентификации, то программист вынужден помнить, для каких именно сочетаний типов существуют перегруженные операции и вручную приводить к ним операнды. Если же язык допускает «приблизительную» идентификацию, никогда нельзя поручиться, что в некоей достаточно сложной ситуации будет выполнен именно тот вариант операции, который имел в виду программист.
- Приоритет и ассоциативность. Если они определены жёстко — это может быть неудобно и не соответствовать предметной области (например, для операций с множествами приоритеты отличаются от арифметических). Если они могут быть заданы программистом — это становится дополнительным источником ошибок (уже хотя бы потому, что разные варианты одной операции оказываются имеющими разные приоритеты, а то и ассоциативность).
Насколько удобство от пользования собственными операциями способно перевесить неудобства от ухудшения управляемости программы — вопрос, не имеющий однозначного ответа.
С позиции реализации языка те же самые проблемы приводят к усложнению трансляторов и понижению их эффективности и надёжности. А использование перегрузки совместно с полиморфными переменными, к тому же заведомо медленнее, чем вызов жёстко прошитой при компиляции операции, и даёт меньше возможностей для оптимизации объектного кода. Отдельной критике подвергаются конкретные особенности реализации перегрузки в различных языках. Так, в C++ объектом критики может стать отстутствие соглашения о внутреннем представление имён перегруженных функций, что порождает несовместимость на уровне бибилиотек, скомпилированных разными компиляторами C++.
Часть критиков высказываются против перегрузки операций, исходя из общих принципов теории разработки программного обеспечения и реальной промышленной практики.
- Сторонники «пуританского» подхода к построению языков, такие как Вирт или Хоар, выступают против перегрузки операций уже просто потому, что без неё можно легко обойтись. По их мнению, подобные средства лишь усложняют язык и транслятор, не предоставляя соответствующих этому усложнению дополнительных возможностей. По их мнению, сама идея создания ориентированного на задачу расширения языка лишь выглядит привлекательно. В действительности же использование средств расширения языка делает программу понятной только её автору — тому, кто это расширение разработал. Программу становится гораздо труднее понимать и анализировать другим программистам, что затрудняет сопровождение, модификацию и групповую разработку.
- Отмечается, что сама возможность использования перегрузки часто играет провоцирующую роль: программисты начинают пользоваться ею где только возможно, в результате средство, призванное упростить и упорядочить программу, становится причиной её усложнения и запутывания.
- Перегруженные операции могут делать не совсем то, что ожидается от них, исходя из их вида. Например, a + b обычно (но не всегда) означает то же самое, что b + a , но «один» + «два» отличается от «два» + «один» в языках, где оператор + перегружен для конкатенациистрок.
- Перегрузка операций делает фрагменты программы более контекстно-зависимыми. Не зная типов участвующих в выражении операндов, невозможно понять, что это выражение делает, если в нём используются перегруженные операции. Например, в программе на C++ оператор
Классификация
Ниже приведена классификация некоторых языков программирования по тому, позволяют ли они перегрузку операторов, и ограничены ли операторы предопределённым набором:
См. также
Wikimedia Foundation . 2010 .