Что такое перегрузка в программировании
Перейти к содержимому

Что такое перегрузка в программировании

  • автор:

Перегрузка

Если точного соответствия ни на одном уровне не найдено, но найдено несколько подходящих функций на разных уровнях, то используется функция, найденная на наименьшем уровне. В пределах одного уровня не может быть более одной подходящей функции.

  • Инкапсуляция и расширяемость типов
  • Наследование
  • Полиморфизм
  • Перегрузка
  • Виртуальные функции
  • Статические члены класса
  • Шаблоны функций
  • Шаблоны классов
  • Абстрактные классы

Присоединяйся к нам — скачай 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<< /////////////////////////////////////////////////////////////////////////////

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. Перегрузка оператора присваивания

Перегрузку оператора присваивания следует выделить особо. Мы уже рассматривали функцию, которая позволяет инициализировать один экземпляр класса значениями переменных членов хранящихся в другом экземпляре. Это конструктор копирования. Оператор присваивания отличается тем, что новый класс при его выполнении не создаётся, а значения членов одного экземпляра присваиваются членам другого экземпляра. При перегрузке оператора присваивания необходимо руководствоваться следующими правилами:

  1. Аргументом перегруженного оператора присваивания должна быть неизменяемая ссылка на экземпляр данного класса (чтобы случайно не испортить экземпляр).
  2. Перед осуществлением присваивания необходимо осуществить проверку на присваивание самому себе (чтобы не выполнять лишних действий).
  3. Оператор должен осуществлять поэлементное присваивание (то есть должно быть выполнено последовательное присваивание каждой переменной).
  4. Оператор должен возвращать ссылку на самого себя (чтобы оно было похоже на присваивание встроенных типов данных, тогда будет возможна запись 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. Перегрузка операторов с присваиванием

Перегрузка операторов с присваиванием (+=, *= и т.д.) осуществляется по следующим правилам:

  1. Аргументом перегруженного оператора с присваиванием должна быть неизменяемая ссылка на экземпляр данного класса (чтобы случайно не испортить экземпляр).
  2. Перегруженный оператор с присваиванием должен возвращать ссылку на самого себя.
// оператор умножения с присваиванием 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 .

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *