Перевантаження операторів в С++: основи, приклади
Приклад перевантаження операторів
Практично як і будь-яка мова, C++ підтримує безліч операторів, що працюють з типами даних, вбудованими в стандарт мови. Але більшість програм використовують спеціальні типи для вирішення тих чи інших завдань. Наприклад, комплексна математика або матрична алгебра реалізуються в програмі за рахунок представлення комплексних чисел або матриць в користувацьких типів C++. Вбудовані оператори не вміють поширювати свою роботу і здійснювати необхідні процедури над одними класами, якими б очевидними вони не здавалися. Тому для додавання матриць, наприклад, зазвичай створюється окрема функція. Очевидно, що виклик функції sum_matrix (A, B) в коді буде носити менш ясний характер, ніж вираз A + B. Розглянемо приблизний клас комплексних чисел: //уявімо комплексне число у вигляді пари чисел з плаваючою точкою.
class complex {
double re, im;
public:
complex (double r, double i) :re(r), im(i) {} //конструктор
complex operator+(complex); //перевантаження складання
complex operator*(complex); //перевантаження множення
};
void main() {
complex a{1 2}, b{3 4}, c{0 0};
c = a + b;
c = a.operator+(b); ////операторна функція може бути викликана будь-яка функція, дана запис еквівалентна a+b
c = a*b + complex(1 3); //Виконуються звичайні правила пріоритету операцій додавання і множення
}
Аналогічним чином можна зробити, наприклад, перевантаження операторів введення/виводу в C++ і пристосувати їх для виведення таких складних структур як матриці.
Оператори, доступні для перевантаження
Повний список всіх операторів, для яких можна використовувати механізм перевантаження:
+ | - | * | / | % | ^ | & |
| | ~ | ! | = | <</p> | > | += |
-= | *= | /= | %= | ^= | &= | |= |
|
| = | = | == | != | <= |
>= | && | || | ++ | -- | ->* | , |
-> | [] | () | new | new[] | delete | delete[] |
Як видно з таблиці, перевантаження допустима для більшості операторів мови. Необхідність перевантаження оператора бути не може. Це робиться виключно для зручності. Тому перевантаження операторів в Java, наприклад, відсутня. А тепер про такому важливому моменті.
Оператори, перевантаження яких заборонена
- Дозвіл області видимості – «::»;
- Вибір члена – «.»;
- Вибір члена через покажчик на член – «.*»;
- Тернарний умовний оператор – «?:»;
- Оператор sizeof;
- Оператор typeid.
Правим операндом даних операторів є ім'я, а не значення. Тому дозвіл їх перевантаження могло б призвести до написання безлічі неоднозначних конструкцій і сильно ускладнило б життя програмістів. Хоча є безліч мов програмування, в яких допускається перевантаження всіх операторів - наприклад, перевантаження операторів Python.
<script type="text/jаvascript">
var blockSettings2 = {blockId:"R-A-70350-2",renderTo:"yandex_rtb_R-A-70350-2",async:!0};
if(document.cookie.indexOf("abmatch=") >= 0){
blockSettings2 = {blockId:"R-A-70350-2",renderTo:"yandex_rtb_R-A-70350-2",statId:70350async:!0};
}
!function(a,b,c,d,e){a[c]=a[c]||[],a[c].push(function(){Ya.Context.AdvManager.render(blockSettings2)}),e=b.getElementsByTagName("script")[0],d=b.createElement("script"),d.type="text/jаvascript",d.src="//an.yandex.ru/system/context.js",d.async=!0e.parentNode.insertBefore(d,e)}(this,this.document,"yandexContextAsyncCallbacks");
Обмеження
Обмеження перевантаження операторів:
- Не можна змінити бінарний оператор на унарний і навпаки, як і не можна додати третій операнд.
- Не можна створювати нові оператори крім тих, що є. Дане обмеження сприяє усуненню безлічі неоднозначностей. Якщо є необхідність у новому операторі, можна використовувати для цих цілей функцію, яка буде виконувати потрібну дію.
- Операторна функція може бути або членом класу, або мати хоча б один аргумент типу. Винятком є оператори new і delete. Таке правило забороняє змінювати сенс виразів у разі, якщо вони не містять об'єктів типів, визначених користувачем. Зокрема, не можна створити операторну функцію, яка працювала б виключно з покажчиками або змусити оператор додавання працювати як множення. Винятком є оператори "=", "&" і "," для об'єктів класів.
- Операторна функція з першим членом, належить до одного з вбудованих типів даних мови C++, не може бути членом класу.
- Назва будь операторної функції починається з ключового слова operator, за яким слід символьне позначення самого оператора.
- Вбудовані оператори визначені таким чином, що між ними буває зв'язок. Наприклад, наступні оператори еквівалентні один одному: ++x; x + = 1; x = x + 1. Після перевизначення зв'язок між ними не збережеться. Про збереження їх спільної роботи подібним чином з новими типами програмісту доведеться піклуватися окремо.
- Компілятор не вміє думати. Вирази z + 5 і 5 +z (де z – комплексне число) будуть розглядатися компілятором по-різному. Перше являє собою «complex + число», а друге - «число + комплекс». Тому для кожного виразу потрібно визначити власний оператор додавання.
- При пошуку визначення оператора компілятор не віддає переваги ні функцій-членів класу, ні допоміжних функцій, які визначаються поза класу. Для компілятора вони рівні.
Інтерпретації бінарних і унарных операторів.
Бінарний оператор визначається як функція-член з однією змінною або як функція з двома змінними. Для будь-якого бінарного оператора @ вираз a@b @ справедливі конструкції:
<script type="text/jаvascript">
var blockSettings3 = {blockId:"R-A-70350-3",renderTo:"yandex_rtb_R-A-70350-3",async:!0};
if(document.cookie.indexOf("abmatch=") >= 0){
blockSettings3 = {blockId:"R-A-70350-3",renderTo:"yandex_rtb_R-A-70350-3",statId:70350async:!0};
}
!function(a,b,c,d,e){a[c]=a[c]||[],a[c].push(function(){Ya.Context.AdvManager.render(blockSettings3)}),e=b.getElementsByTagName("script")[0],d=b.createElement("script"),d.type="text/jаvascript",d.src="//an.yandex.ru/system/context.js",d.async=!0e.parentNode.insertBefore(d,e)}(this,this.document,"yandexContextAsyncCallbacks");
a.operator@(b) або operator@(a, b).
Розглянемо на прикладі класу комплексних чисел визначення операцій як членів класу і допоміжних.
class complex {
double re, im;
public:
complex& operator+=(complex z);
complex& operator*=(complex z);
};
//допоміжні функції
complex operator+(complex z1 complex z2);
complex operator+(complex z, double a);
Який з операторів буде обраний, і чи буде взагалі обраний, визначається внутрішніми механізмами мови, про яких мова піде нижче. Зазвичай це відбувається за відповідністю типів.
Вибір, описувати функцію як член класу або поза його – справа, загалом-то, смаку. У прикладі вище принцип відбору був наступний: якщо операція змінює лівий операнд (наприклад, a + = b), то записати її всередині класу і використовувати передачу змінної за адресою, для її безпосереднього зміни; якщо операція нічого не змінює і просто повертає нове значення (наприклад, a + b) – винести за рамки визначення класу.
Визначення перевантаження унарных операторів в C++ відбувається аналогічним чином, з тією різницею, що вони діляться на два види:
- префіксний оператор, розташований до операнда, – @a, наприклад, i++. o визначається як a.operator@() або operator@(aa);
- постфиксный оператор, розташований після операнда, – b@, наприклад, i++. o визначається як b.operator@(int) або operator@(b, int)
Точно так само, як і з бінарними операторами для випадку, коли оголошення оператора знаходиться і в класі, і поза класу, вибір буде здійснюватися механізмами C++.
Правила вибору оператора
Нехай бінарний оператор @ застосовується до об'єктів x класу X і y з класу Y. Правила для дозволу x@y будуть наступні:
- якщо X являє собою клас, шукати всередині нього визначення оператора operator@ в якості члена X, або базового класу X;
- переглянути контекст, в якому знаходиться вираз x@y;
- якщо X належить до простору імен N, шукати оголошення оператора N;
- якщо Y відноситься до простору імен M, шукати оголошення оператора M.
У разі якщо в 1-4 було знайдено кілька оголошень оператора operator@, вибір буде здійснюватися за правилами дозволу перевантажених функцій.
Пошук оголошень унарных операторів відбувається точно таким же способом.
Уточнене визначення класу complex
Тепер побудуємо клас комплексних чисел більш докладним чином, щоб продемонструвати низка озвучених раніше правил.
class complex {
double re, im;
public:
complex& operator+=(complex z) {//працює з виразами виду z1 += z2
re += z.re;
im += z.im;
return *this;
}
complex& operator+=(double a) {//працює з виразами виду z1 += 5;
re += a;
return *this;
}
complex (): re(0), im(0) {} //конструктор для ініціалізації за замовчуванням. Таким чином, всі оголошені комплексні числа будуть мати початкові значення (0 0)
complex (double r): re(r), im(0) {} //конструктор робить можливим вираз виду complex z = 11; еквівалентна запис z = complex(11);
complex (double r, double i): re(r), im(i) {} //конструктор
};
complex operator+(complex z1 complex z2) {//працює з виразами виду z1 + z2
complex res = z1;
return res += z2; //використання оператора, визначеного як функція-член
}
complex operator+(complex z, double a) {//обробляє вирази виду z+2
complex res = z;
return res += a;
}
complex operator+(double a, complex z) {//обробляє вирази виду 7+z
complex res = z;
return res += a;
}
//
Як видно з коду, перевантаження операторів має досить складний механізм, який може сильно розростися. Однак такий детальний підхід дозволяє здійснювати перевантаження навіть для дуже складних структур даних. Наприклад, перевантаження операторів C++ в класі шаблонів. Подібне створення функцій для всіх і вся може бути стомлюючим і приводити до помилок. Наприклад, якщо додати третій тип розглянуті функції, то потрібно буде розглянути операції з міркувань поєднання трьох типів. Доведеться написати 3 функції з одним аргументом, 9 – з двома і 27 – з трьома. Тому в ряді випадків реалізація всіх цих функцій і значне зменшення їх кількості можуть бути досягнуті за рахунок використання перетворення типів.