На цьому уроці ми розглянемо перевантаження оператора ()
в мові С++.
Перевантаження оператора ()
Всі оператори, перевантаження яких ми розглядали дотепер, дозволяли нам самостійно визначати тип параметрів в функції перевантаження оператора, але не їх кількість. Наприклад, оператор ==
завжди приймає два параметри, тоді як оператор !
завжди приймає один параметр. Оператор ()
є особливо цікавим, оскільки дозволяє змінювати як тип параметрів, так і їх кількість.
Але слід пам’ятати про дві речі:
По-перше, перевантаження круглих дужок повинне здійснюватися через метод класу.
По-друге, в не об’єктно-орієнтованому С++ оператор ()
є оператором виклику функції. У випадку з класами перевантаження круглих дужок виконується в методі operator()(){}
(в оголошенні функції перевантаження знаходяться дві пари круглих дужок).
Розглянемо наступний клас:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Matrix { private: double data[5][5]; public: Matrix() { // Присвоюємо всім елементам масиву значення 0.0 for (int row=0; row < 5; ++row) for (int col=0; col < 5; ++col) data[row][col] = 0.0; } }; |
Матриці є ключовою концепцією в лінійній алгебрі та часто використовуються в геометричному моделюванні і в 3D-графіці. Все, що вам потрібно зараз знати — це те, що клас Matrix є двовимірним масивом (5×5 типу double).
На уроці про перевантаження оператора індексації ми використовували оператор []
для прямого доступу до елементів закритого одновимірного масиву. Тут же нам потрібен доступ до елементів двовимірного масиву. Оскільки оператор []
обмежений лише одним параметром, то його функціональності недостатньо для доступу до двовимірного масиву.
Однак, оскільки оператор ()
може приймати різну кількість параметрів, ми можемо оголосити версію operator(), яка прийматиме два цілочисельних параметри (два індекси), і використовуватиме ці індекси для доступу до елементів нашого двовимірного масиву. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <iostream> #include <cassert> // для assert() class Matrix { private: double data[5][5]; public: Matrix() { // Присвоюємо всім елементам масиву значення 0.0 for (int row=0; row < 5; ++row) for (int col=0; col < 5; ++col) data[row][col] = 0.0; } double& operator()(int row, int col); const double& operator()(int row, int col) const; // для константних об'єктів }; double& Matrix::operator()(int row, int col) { assert(col >= 0 && col < 5); assert(row >= 0 && row < 5); return data[row][col]; } const double& Matrix::operator()(int row, int col) const { assert(col >= 0 && col < 5); assert(row >= 0 && row < 5); return data[row][col]; } int main() { Matrix matrix; matrix(2, 3) = 3.6; std::cout << matrix(2, 3); return 0; } |
Результат виконання програми:
3.6
Виконаємо перевантаження оператора ()
ще раз, але вже без використання будь-яких параметрів:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#include <iostream> #include <cassert> // для assert() class Matrix { private: double data[5][5]; public: Matrix() { // Присвоюємо всім елементам масиву значення 0.0 for (int row=0; row < 5; ++row) for (int col=0; col < 5; ++col) data[row][col] = 0.0; } double& operator()(int row, int col); const double& operator()(int row, int col) const; void operator()(); }; double& Matrix::operator()(int row, int col) { assert(col >= 0 && col < 5); assert(row >= 0 && row < 5); return data[row][col]; } const double& Matrix::operator()(int row, int col) const { assert(col >= 0 && col < 5); assert(row >= 0 && row < 5); return data[row][col]; } void Matrix::operator()() { // Скидаємо значення всіх елементів масиву на 0.0 for (int row=0; row < 5; ++row) for (int col=0; col < 5; ++col) data[row][col] = 0.0; } int main() { Matrix matrix; matrix(2, 3) = 3.6; matrix(); // виконуємо скидання std::cout << matrix(2, 3); return 0; } |
Результат виконання програми:
0
Оскільки оператор ()
є дуже гнучким, то може виникнути спокуса використати його для самих різних цілей. Однак це не рекомендується робити, оскільки оператор ()
не є дуже інформативним і часто може бути не зрозуміло, що він робить. У прикладі, наведеному вище, функцію очищення масиву краще було б записати у вигляді методу clear() або erase(), оскільки matrix.erase()
виглядає набагато інформативніше, ніж matrix()
.
Функтори в C++
Перевантаження оператора ()
використовується в реалізації функторів (або “функціональних об’єктів”) — класи, які працюють як функції. Перевага функтора над звичайною функцією полягає в тому, що функтори можуть зберігати дані в змінних-членах (оскільки вони самі є класами). Ось приклад використання простого функтора:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> class Accumulator { private: int m_counter = 0; public: Accumulator() { } int operator() (int i) { return (m_counter += i); } }; int main() { Accumulator accum; std::cout << accum(30) << std::endl; // виведеться 30 std::cout << accum(40) << std::endl; // виведеться 70 return 0; } |
Зверніть увагу, використання класу Accumulator виглядає так само, як і виклик звичайної функції, але наш об’єкт класу Accumulator може зберігати значення, яке збільшується.
Ви можете запитати: “Навіщо використовувати клас, якщо все можна реалізувати і через звичайну функцію зі статичною локальною змінною?”. Можна зробити і через static, але, оскільки функції представлені тільки одним глобальним екземпляром (тобто не можна створити кілька об’єктів функції), використовувати цю функцію ми можемо тільки для виконання чогось одного за раз. За допомогою функторів ми можемо створити будь-яку кількість окремих функціональних об’єктів, які нам потрібні, і використовувати їх одночасно.
Висновки
Перевантаження оператора ()
з двома параметрами використовується для отримання доступу до двовимірних масивів або для повернення підмножин одновимірного масиву (два параметри конкретизуватимуть умови відбору елементів підмножини). Все інше краще реалізувати через окремі методи з більш інформативними назвами, ніж через перевантаження оператора ()
.
Перевантаження оператора ()
також часто використовується при створенні функторів. Хоча функтори, які ми використовували вище, є досить простими і зрозумілими, але зазвичай вони використовуються в більш просунутих/складних темах програмування і заслуговують окремого уроку.
Тест
Напишіть клас, змінною-членом якого є рядок. Виконайте перевантаження оператора ()
для повернення підрядка, який починається з індексу, вказаного в значенні першого параметру. Другий параметр повинен вказувати необхідну довжину підрядка.
Підказки:
Ви можете використати індекс масиву []
для доступу до окремих символів рядка.
Ви можете використати оператор +=
для додання чого-небудь до рядка.
Наступний фрагмент коду:
1 2 3 4 5 6 7 |
int main() { Mystring string("Hello, world!"); std::cout << string(7, 6); // починаємо з 7 символу (індексу) і повертаємо наступні 6 символів return 0; } |
Повинен видавати наступний результат:
world!
Відповідь
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <iostream> #include <string> class Mystring { private: std::string m_string; public: Mystring(const std::string string="") :m_string(string) { } std::string operator()(int index1, int length) { std::string ret; for (int count = 0; count < length; ++count) ret += m_string[index1 + count]; return ret; } }; int main() { Mystring string("Hello, world!"); std::cout << string(7, 6); // починаємо з 7-го символу (індексу) і повертаємо наступні 6 символів return 0; } |