У будь-якій науці є стандартні позначення, які полегшують розуміння ідей. Наприклад, в математиці це множення, ділення, додавання і інші символьні позначення. Вираз (x + y * z) зрозуміти куди простіше, ніж «помножити y, z і додати до x». Уявіть, до XVI століття математика не мала символьних позначень, всі вирази прописувалися словесно так, ніби це художній текст з описом. А звичні для нас позначення операцій з'явилися і того пізніше. Значення короткої символьного запису складно переоцінити. Виходячи з таких міркувань, мови програмування були додані перевантаження операторів. Розглянемо на прикладі.
Приклад перевантаження операторів
Практично як і будь-яка мова, 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 – з трьома. Тому в ряді випадків реалізація всіх цих функцій і значне зменшення їх кількості можуть бути досягнуті за рахунок використання перетворення типів.
Особливі оператори
Оператор індексації«[]» повинен завжди визначатися як член класу, так як зводить поведінку об'єкта до масиву. При цьому аргумент індексування може бути будь-якого типу, що дозволяє створювати, наприклад, асоціативні масиви. Оператор виклику функції «()» може розглядатися як бінарна операція. Наприклад, у конструкції «вираз(список виразів)» лівим операндом бінарної операції () буде «вираз», а правим – список виразів. Функція operator()() повинна бути членом класу. Оператор послідовності «,» (кома) викликається для об'єктів, якщо поряд з ними є кома. Однак у перерахуванні аргументів функції оператор не бере участь. Оператор розіменування «->» також повинен визначатися в якості члена функції. За своїм змістом його можна визначити як унарний постфиксный оператор. При цьому він в обов'язковому порядку повинен повертати або посилання, або покажчик, що дозволяє звертатися до об'єкта. Оператор присвоювання також визначається тільки в якості члена класу з-за його зв'язку з лівим операндом. Оператори присвоювання «=», адреси «&» і послідовності «,» повинні визначатися в блоці public.
Підсумок
Перевантаження операторів допомагає реалізувати один з ключових аспектів ООП про поліморфізм. Але важливо розуміти, що перевантаження – це не більше ніж інший спосіб виклику функцій. Завдання перевантаження операторів часто полягає в поліпшенні розуміння коду, ніж в забезпеченні виграшу в якихось питаннях.
І це ще не все. Також слід враховувати, що перевантаження операторів являє собою складний механізм з безліччю підводних каменів. Тому дуже легко допустити помилку. Це є основною причиною, по якій більшість програмістів радять утриматися від використання перевантаження операторів і вдаватися до неї тільки в крайньому випадку і з повною впевненістю у своїх діях.
Рекомендації
Виконуйте перевантаження операторів тільки для імітації звичної запису. Для того щоб зробити код читання. Якщо код стає складніше за структурою або читабельності, слід відмовитися від перевантаження операторів і використовувати функції. Для великих операндів з метою економії місця використовуйте для передачі аргументи з типом константных посилань. Оптимізуйте значення, що повертаються. Не чіпайте операцію копіювання, якщо вона підходить для вашого класу. Якщо копіювання за замовчуванням не підходить, міняйте або явно забороняйте можливість копіювання. Слід віддавати перевагу функції-члени над функціями-нечленами у випадках, коли функції потрібен доступ до поданням класу. Вказуйте простір імен і позначайте зв'язок функцій з класом. Використовуйте функції-нечлены для симетричних операторів. Використовуйте оператор () для індексів в багатовимірних масивах. З обережністю використовуйте неявні перетворення.