C++

Перевантаження операторів: методи класу та спеціальні оператори

Унарні оператори (-, !, ~), інкремент та декремент (++, --), оператор індексації [], оператор виклику (), оператори присвоювання. Перевантаження через методи класу.

Перевантаження операторів: методи класу та спеціальні оператори

Від бінарних до унарних: нова категорія операторів

У попередній статті ми вивчили перевантаження бінарних операторів — операторів із двома операндами (a + b, a < b, std::cout << a). Ці оператори природно реалізовувати як friend-функції, бо обидва операнди мають рівну роль.

Але C++ має і унарні оператори — оператори з одним операндом. Також є спеціальні оператори, що працюють зовсім інакше: оператор індексації [], оператор виклику функції (), оператори присвоювання =, +=, -=. Ці оператори тісно пов'язані з об'єктом, що стоїть зліва, тому їх природно реалізовувати як методи класу.

Розглянемо клас Counter — простий лічильник, що демонструє всі категорії операторів:

CounterBasic.cpp
#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter(int v = 0) : value(v) {}

    int getValue() const
    {
        return value;
    }

    void print() const
    {
        std::cout << "Counter: " << value;
    }
};

int main()
{
    Counter count(5);

    count.print();
    std::cout << "\n";

    // ❌ Хочемо писати:
    // count++;        // Інкремент
    // -count;         // Унарний мінус
    // count[0];       // Індексація (якщо це масив лічильників)
    // count(10);      // Виклик як функції

    // Але без перевантаження це не працює

    return 0;
}

Давайте навчимо клас Counter розуміти ці оператори.

Методи класу vs friend-функції: коли оператор перевантажується як метод класу, лівий операнд стає неявним об'єктом this. Правий операнд (якщо є) передається як параметр. Це природно для операторів, де лівий операнд є «головним» — він модифікується або до нього звертаються.

Унарні оператори: -, !, ~

Унарні оператори мають один операнд. Найпоширеніші:

  • Унарний мінус (-a) — негація числа
  • Логічне НЕ (!a) — інверсія булевого значення
  • Побітове НЕ (~a) — інверсія бітів (рідко використовується для класів)

Унарні оператори перевантажуються як const-методи без параметрів:

UnaryOperators.cpp
#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter(int v = 0) : value(v) {}

    int getValue() const { return value; }

    // Унарний мінус: повертає новий Counter із від'ємним значенням
    Counter operator-() const
    {
        return Counter(-value);
    }

    // Логічне НЕ: повертає true, якщо лічильник дорівнює 0
    bool operator!() const
    {
        return value == 0;
    }

    void print() const
    {
        std::cout << "Counter: " << value;
    }
};

int main()
{
    Counter count(5);
    Counter zero(0);

    std::cout << "Original: ";
    count.print();
    std::cout << "\n";

    // Унарний мінус
    Counter negated = -count;
    std::cout << "Negated: ";
    negated.print();
    std::cout << "\n";

    // Логічне НЕ
    if (!zero)
    {
        std::cout << "Zero counter is empty (evaluates to true)\n";
    }

    if (!count)
    {
        std::cout << "This won't print\n";
    }
    else
    {
        std::cout << "Non-zero counter is not empty\n";
    }

    return 0;
}
./UnaryOperators
$ ./UnaryOperators
Original: Counter: 5
Negated: Counter: -5
Zero counter is empty (evaluates to true)
Non-zero counter is not empty
Execution finished with exit code 0.

Рядки 14–17: operator-() — const-метод без параметрів. Створює новий Counter із від'ємним значенням. Вираз -count компілятор перетворює на count.operator-().

Рядки 20–23: operator!() повертає bool. Логіка: лічильник дорівнює нулю — це «порожній» стан, що логічно інтерпретується як false. Тому !zero дає true.

Рядок 43: вираз -count створює анонімний тимчасовий об'єкт Counter(-5), який присвоюється змінній negated.

Семантика унарного мінусу: для числових типів (int, double) унарний мінус змінює знак. Для користувацьких типів семантика залежить від контексту. Для Counter — змінює знак значення. Для Vector2D — інвертує напрямок вектора. Для Money — змінює знак (борг vs актив).

Інкремент та декремент: ++ та --

Оператори інкременту (++) та декременту (--) існують у двох формах:

  • Префіксна (++a, --a) — збільшує/зменшує значення, повертає посилання на змінений об'єкт
  • Постфіксна (a++, a--) — збільшує/зменшує значення, повертає копію старого стану

Компілятор розрізняє ці форми за сигнатурою:

// Префіксна форма: без параметрів, повертає посилання
Counter& operator++();      // ++count

// Постфіксна форма: фіктивний параметр int, повертає копію
Counter operator++(int);    // count++

Фіктивний параметр int у постфіксній формі — це маркер для компілятора, а не реальний аргумент. Його значення не використовується.

IncrementDecrement.cpp
#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter(int v = 0) : value(v) {}

    int getValue() const { return value; }

    // === Префіксний інкремент (++count) ===
    Counter& operator++()
    {
        ++value;        // Збільшуємо значення
        return *this;   // Повертаємо посилання на себе
    }

    // === Постфіксний інкремент (count++) ===
    Counter operator++(int)
    {
        Counter temp(*this);  // Зберігаємо копію поточного стану
        ++(*this);            // Викликаємо префіксну версію
        return temp;          // Повертаємо старий стан
    }

    // === Префіксний декремент (--count) ===
    Counter& operator--()
    {
        --value;
        return *this;
    }

    // === Постфіксний декремент (count--) ===
    Counter operator--(int)
    {
        Counter temp(*this);
        --(*this);
        return temp;
    }

    void print() const
    {
        std::cout << "Counter: " << value;
    }
};

int main()
{
    Counter count(5);

    std::cout << "Initial: ";
    count.print();
    std::cout << "\n";

    // Префіксний інкремент: збільшує і повертає нове значення
    ++count;
    std::cout << "After ++count: ";
    count.print();
    std::cout << "\n";

    // Постфіксний інкремент: збільшує, але повертає старе значення
    Counter old = count++;
    std::cout << "After count++, old: ";
    old.print();
    std::cout << ", current: ";
    count.print();
    std::cout << "\n";

    // Префіксний декремент
    --count;
    std::cout << "After --count: ";
    count.print();
    std::cout << "\n";

    // Постфіксний декремент
    Counter old2 = count--;
    std::cout << "After count--, old: ";
    old2.print();
    std::cout << ", current: ";
    count.print();
    std::cout << "\n";

    return 0;
}
./IncrementDecrement
$ ./IncrementDecrement
Initial: Counter: 5
After ++count: Counter: 6
After count++, old: Counter: 6, current: Counter: 7
After --count: Counter: 6
After count--, old: Counter: 6, current: Counter: 5
Execution finished with exit code 0.

Рядки 14–18: префіксний operator++() збільшує value і повертає *this (посилання на поточний об'єкт). Це ефективно — без копіювання.

Рядки 21–26: постфіксний operator++(int) зберігає копію старого стану у temp, викликає префіксну версію для збільшення, повертає temp. Це менш ефективно через копіювання.

Рядок 24: ++(*this) викликає префіксну версію operator++() для поточного об'єкта. Уникаємо дублювання коду.

Рядки 67–68: Counter old = count++ демонструє різницю: count збільшується до 7, але old отримує старе значення 6.

Продуктивність: префікс vs постфікс. Постфіксна форма завжди повільніша за префіксну через створення тимчасової копії. Для примітивних типів (int, double) компілятор оптимізує це, але для класів копіювання може бути дорогим (особливо якщо клас керує динамічною пам'яттю). Правило: якщо вам не потрібно старе значення, використовуйте префіксну форму ++count замість count++.

Оператор індексації: []

Оператор індексації [] дозволяє звертатися до елементів контейнера як до масиву: array[i]. Щоб підтримувати як читання, так і запис, потрібно дві версії оператора:

  1. Не-const версія — повертає тип& (посилання), дозволяє модифікацію
  2. Const версія — повертає const тип& або тип, дозволяє лише читання
ArrayWrapper.cpp
#include <iostream>
#include <cassert>

class IntArray
{
private:
    int data[10];
    int size;

public:
    IntArray() : size(10)
    {
        for (int i = 0; i < size; ++i)
        {
            data[i] = 0;
        }
    }

    int getSize() const { return size; }

    // Не-const версія: дозволяє запис array[i] = value
    int& operator[](int index)
    {
        assert(index >= 0 && index < size && "Index out of bounds");
        return data[index];
    }

    // Const версія: дозволяє читання з const об'єктів
    const int& operator[](int index) const
    {
        assert(index >= 0 && index < size && "Index out of bounds");
        return data[index];
    }
};

int main()
{
    IntArray array;

    // Запис через не-const версію
    array[0] = 10;
    array[1] = 20;
    array[5] = 50;

    std::cout << "array[0] = " << array[0] << "\n";
    std::cout << "array[1] = " << array[1] << "\n";
    std::cout << "array[5] = " << array[5] << "\n";

    // Const об'єкт — викликається const версія operator[]
    const IntArray constArray;
    // constArray[0] = 10;  // ❌ ПОМИЛКА: не можна змінювати const об'єкт
    std::cout << "constArray[0] = " << constArray[0] << "\n";  // ✅ Читання дозволено

    return 0;
}
./ArrayWrapper
$ ./ArrayWrapper
array[0] = 10
array[1] = 20
array[5] = 50
constArray[0] = 0
Execution finished with exit code 0.

Рядки 22–26: не-const operator[] повертає int& — посилання на елемент. Це дозволяє писати array[0] = 10 (використання як l-value).

Рядки 29–33: const operator[] повертає const int& — константне посилання. Це дозволяє читати елемент, але не змінювати його.

Рядок 24, 31: assert() перевіряє коректність індексу під час виконання. У Debug-режимі це викличе зупинку програми при виході за межі. У Release-режимі (з -DNDEBUG) assert вимикається.

Рядок 41–43: вирази array[i] = value компілятор перетворює на array.operator[](i) = value. Оскільки operator[] повертає посилання, присвоювання працює.

Рядок 53: для constArray викликається const версія operator[], яка повертає const int&. Спроба запису (рядок 52, закоментований) викликає помилку компіляції.

Чому дві версії? Компілятор вибирає версію operator[] залежно від того, чи є об'єкт константним. Для const IntArray викликається const-метод. Це частина системи const-коректності C++ — механізму, що гарантує, що константні об'єкти не можуть бути змінені.

Оператор виклику функції: () та функтори

Оператор виклику функції () дозволяє об'єкту поводитися як функція. Об'єкти з перевантаженим operator() називаються функторами (functors) або об'єктами-функціями (function objects).

Чому це корисно? Функтори можуть зберігати стан між викликами, на відміну від звичайних функцій. Це робить їх потужнішими за функції у певних сценаріях (наприклад, у алгоритмах STL).

Functors.cpp
#include <iostream>

// Функтор-акумулятор: зберігає суму між викликами
class Accumulator
{
private:
    int total;

public:
    Accumulator() : total(0) {}

    // Оператор виклику: додає значення до суми
    int operator()(int value)
    {
        total += value;
        return total;
    }

    void reset()
    {
        total = 0;
    }
};

// Функтор-множник: зберігає коефіцієнт
class Multiplier
{
private:
    int factor;

public:
    Multiplier(int f) : factor(f) {}

    // Оператор виклику: множить аргумент на коефіцієнт
    int operator()(int value) const
    {
        return value * factor;
    }
};

int main()
{
    // Акумулятор: зберігає суму
    Accumulator sum;

    std::cout << "Accumulating:\n";
    std::cout << "sum(10) = " << sum(10) << "\n";  // 10
    std::cout << "sum(20) = " << sum(20) << "\n";  // 30
    std::cout << "sum(5) = " << sum(5) << "\n";    // 35

    sum.reset();
    std::cout << "After reset, sum(15) = " << sum(15) << "\n";  // 15

    // Множник: створюємо кілька функторів із різними коефіцієнтами
    Multiplier timesTwo(2);
    Multiplier timesTen(10);

    std::cout << "\nMultiplying:\n";
    std::cout << "timesTwo(5) = " << timesTwo(5) << "\n";    // 10
    std::cout << "timesTwo(8) = " << timesTwo(8) << "\n";    // 16
    std::cout << "timesTen(3) = " << timesTen(3) << "\n";    // 30

    return 0;
}
./Functors
$ ./Functors
Accumulating:
sum(10) = 10
sum(20) = 30
sum(5) = 35
After reset, sum(15) = 15
Multiplying:
timesTwo(5) = 10
timesTwo(8) = 16
timesTen(3) = 30
Execution finished with exit code 0.

Рядки 13–17: operator() приймає один параметр int value, додає його до total, повертає суму. Вираз sum(10) компілятор перетворює на sum.operator()(10).

Рядки 48–50: кожен виклик sum(...) збільшує внутрішній стан total. Це демонструє збереження стану — ключову перевагу функторів над звичайними функціями.

Рядки 57–58: створюємо два різні функтори Multiplier із різними коефіцієнтами. Кожен має власний стан (factor). Це як створити кілька «параметризованих функцій».

Рядки 61–63: виклики timesTwo(5) та timesTen(3) демонструють, що функтори можуть мати різну поведінку залежно від їхнього стану.

Функтори у стандартній бібліотеці: алгоритми STL (std::sort, std::find_if, std::transform) активно використовують функтори. Наприклад, std::sort(vec.begin(), vec.end(), std::greater<int>()) сортує за спаданням, де std::greater<int> — це функтор. У C++11 з'явилися лямбда-вирази, що є синтаксичним цукром для анонімних функторів.

Оператори присвоювання: =, +=, -=, *=, /=

Оператори присвоювання модифікують лівий операнд і повертають посилання на нього для підтримки ланцюгових присвоювань (a = b = c). Ці оператори завжди реалізуються як методи класу.

Базовий оператор присвоювання =

Оператор присвоювання = викликається при a = b, коли a вже існує (на відміну від конструктора копіювання, що викликається при ініціалізації Counter a = b).

AssignmentOperator.cpp
#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter(int v = 0) : value(v) {}

    int getValue() const { return value; }

    // Оператор присвоювання
    Counter& operator=(const Counter& other)
    {
        // Перевірка самоприсвоювання (self-assignment check)
        if (this == &other)
        {
            return *this;
        }

        // Копіюємо дані
        value = other.value;

        // Повертаємо посилання на себе для ланцюгових присвоювань
        return *this;
    }

    void print() const
    {
        std::cout << "Counter: " << value;
    }
};

int main()
{
    Counter a(10);
    Counter b(20);
    Counter c(30);

    std::cout << "Initial values:\n";
    std::cout << "a = "; a.print(); std::cout << "\n";
    std::cout << "b = "; b.print(); std::cout << "\n";
    std::cout << "c = "; c.print(); std::cout << "\n\n";

    // Просте присвоювання
    a = b;
    std::cout << "After a = b:\n";
    std::cout << "a = "; a.print(); std::cout << "\n\n";

    // Ланцюгове присвоювання: a = b = c виконується справа наліво
    a = b = c;
    std::cout << "After a = b = c:\n";
    std::cout << "a = "; a.print(); std::cout << "\n";
    std::cout << "b = "; b.print(); std::cout << "\n";

    // Самоприсвоювання — має бути безпечним
    a = a;
    std::cout << "\nAfter a = a (self-assignment):\n";
    std::cout << "a = "; a.print(); std::cout << "\n";

    return 0;
}
./AssignmentOperator
$ ./AssignmentOperator
Initial values:
a = Counter: 10
b = Counter: 20
c = Counter: 30
After a = b:
a = Counter: 20
After a = b = c:
a = Counter: 30
b = Counter: 30
After a = a (self-assignment):
a = Counter: 30
Execution finished with exit code 0.

Рядки 14–27: operator= повертає Counter& — посилання на поточний об'єкт. Це дозволяє ланцюгові присвоювання.

Рядки 17–20: перевірка самоприсвоювання if (this == &other). Якщо a = a, одразу повертаємо *this. Це оптимізація та захист від помилок (важливо для класів із динамічною пам'яттю).

Рядок 26: return *this; — повертаємо посилання на поточний об'єкт. Компілятор перетворює a = b на a.operator=(b), що повертає a.

Рядок 53: ланцюгове присвоювання a = b = c виконується справа наліво: спочатку b = c (повертає b), потім a = b. Завдяки поверненню *this це працює.

Рядок 59: самоприсвоювання a = a — рідкісний сценарій, але перевірка гарантує коректність.

Самоприсвоювання та динамічна пам'ять: для класів із динамічною пам'яттю (як std::string, що містить char*) самоприсвоювання без перевірки може призвести до катастрофи. Якщо operator= спочатку звільняє стару пам'ять (delete[]), а потім копіює з other (що насправді this), ви копіюєте зі звільненої пам'яті — undefined behavior. Перевірка if (this == &other) запобігає цьому.

Складені оператори присвоювання: +=, -=, *=, /=

Складені оператори присвоювання модифікують об'єкт і повертають посилання на нього:

CompoundAssignment.cpp
#include <iostream>

class Counter
{
private:
    int value;

public:
    Counter(int v = 0) : value(v) {}

    int getValue() const { return value; }

    // Оператор +=
    Counter& operator+=(int amount)
    {
        value += amount;
        return *this;
    }

    // Оператор -=
    Counter& operator-=(int amount)
    {
        value -= amount;
        return *this;
    }

    // Оператор *=
    Counter& operator*=(int multiplier)
    {
        value *= multiplier;
        return *this;
    }

    // Оператор /=
    Counter& operator/=(int divisor)
    {
        if (divisor != 0)
        {
            value /= divisor;
        }
        else
        {
            std::cerr << "Error: Division by zero\n";
        }
        return *this;
    }

    void print() const
    {
        std::cout << "Counter: " << value;
    }
};

int main()
{
    Counter count(10);

    std::cout << "Initial: ";
    count.print();
    std::cout << "\n";

    // Використання складених операторів
    count += 5;
    std::cout << "After += 5: ";
    count.print();
    std::cout << "\n";

    count -= 3;
    std::cout << "After -= 3: ";
    count.print();
    std::cout << "\n";

    count *= 2;
    std::cout << "After *= 2: ";
    count.print();
    std::cout << "\n";

    count /= 4;
    std::cout << "After /= 4: ";
    count.print();
    std::cout << "\n";

    // Ланцюгові операції
    count += 10;
    count *= 2;
    std::cout << "After += 10, *= 2: ";
    count.print();
    std::cout << "\n";

    return 0;
}
./CompoundAssignment
$ ./CompoundAssignment
Initial: Counter: 10
After += 5: Counter: 15
After -= 3: Counter: 12
After *= 2: Counter: 24
After /= 4: Counter: 6
After += 10, *= 2: Counter: 32
Execution finished with exit code 0.

Рядки 14–18: operator+= додає значення до value і повертає *this. Вираз count += 5 еквівалентний count.operator+=(5).

Рядки 35–46: operator/= містить перевірку ділення на нуль. Якщо divisor == 0, виводить помилку і залишає значення незмінним.

Рядки 88–90: складені оператори можна викликати послідовно. Кожен модифікує об'єкт і повертає посилання, що дозволяє наступний виклик.

Зв'язок між + та +=: якщо у вас є operator+=, можна реалізувати operator+ через нього:
Counter operator+(Counter left, int right)
{
    left += right;  // Модифікуємо копію
    return left;    // Повертаємо модифіковану копію
}
Це зменшує дублювання коду. Аналогічно для - через -=, * через *= тощо.

Які оператори ОБОВ'ЯЗКОВО методи класу?

Не всі оператори можна перевантажувати як вільні функції. Деякі оператори обов'язково мають бути методами класу:

= Присвоювання

operator= — базовий оператор присвоювання. Компілятор генерує версію за замовчуванням, але для класів із динамічною пам'яттю потрібна власна реалізація.

Чому метод? Лівий операнд завжди є об'єктом класу, що модифікується.

[] Індексація

operator[] — доступ до елементів контейнера. Повертає посилання для модифікації або const-посилання для читання.

Чому метод? Індексація тісно пов'язана зі структурою об'єкта.

() Виклик

operator() — функтори, об'єкти-функції. Дозволяє об'єкту поводитися як функція.

Чому метод? Виклик завжди застосовується до конкретного об'єкта.

-> Доступ

operator-> — доступ до членів через об'єкт (використовується у розумних покажчиках, як std::unique_ptr).

Чому метод? Операція доступу до членів завжди застосовується до об'єкта.

Спроба перевантажити ці оператори як friend-функції призведе до помилки компіляції. Компілятор очікує, що =, [], (), -> будуть методами класу. Це жорстке обмеження мови C++.

Повний приклад: клас Money з усіма операторами

Об'єднаємо все, що ми вивчили, у розширений клас Money із унарними операторами, інкрементом та складеними присвоюваннями:

MoneyAdvanced.cpp
#include <iostream>

class Money
{
private:
    int dollars;
    int cents;

    void normalize()
    {
        if (cents >= 100)
        {
            dollars += cents / 100;
            cents = cents % 100;
        }
        else if (cents < 0)
        {
            int dollarsBorrowed = (-cents / 100) + 1;
            dollars -= dollarsBorrowed;
            cents += dollarsBorrowed * 100;
        }
    }

public:
    Money(int d = 0, int c = 0) : dollars(d), cents(c)
    {
        normalize();
    }

    int totalCents() const
    {
        return dollars * 100 + cents;
    }

    // === Унарні оператори ===

    Money operator-() const
    {
        return Money(-dollars, -cents);
    }

    bool operator!() const
    {
        return dollars == 0 && cents == 0;
    }

    // === Інкремент/декремент ===

    Money& operator++()  // Префікс: додає $1
    {
        ++dollars;
        return *this;
    }

    Money operator++(int)  // Постфікс
    {
        Money temp(*this);
        ++(*this);
        return temp;
    }

    Money& operator--()  // Префікс: віднімає $1
    {
        --dollars;
        return *this;
    }

    Money operator--(int)  // Постфікс
    {
        Money temp(*this);
        --(*this);
        return temp;
    }

    // === Складені присвоювання ===

    Money& operator+=(const Money& other)
    {
        dollars += other.dollars;
        cents += other.cents;
        normalize();
        return *this;
    }

    Money& operator-=(const Money& other)
    {
        dollars -= other.dollars;
        cents -= other.cents;
        normalize();
        return *this;
    }

    // === Вивід ===
    friend std::ostream& operator<<(std::ostream& out, const Money& money);
};

std::ostream& operator<<(std::ostream& out, const Money& money)
{
    out << "$" << money.dollars << ".";
    if (money.cents < 10) out << "0";
    out << money.cents;
    return out;
}

int main()
{
    Money wallet(50, 75);

    std::cout << "Initial wallet: " << wallet << "\n";

    // Унарний мінус
    Money debt = -wallet;
    std::cout << "Debt (negated): " << debt << "\n";

    // Логічне НЕ
    Money empty;
    if (!empty)
    {
        std::cout << "Empty wallet evaluates to true with !\n";
    }

    // Інкремент
    ++wallet;
    std::cout << "After ++wallet: " << wallet << "\n";

    Money old = wallet++;
    std::cout << "After wallet++ (old): " << old << ", current: " << wallet << "\n";

    // Декремент
    --wallet;
    std::cout << "After --wallet: " << wallet << "\n";

    // Складені присвоювання
    Money bonus(10, 50);
    wallet += bonus;
    std::cout << "After += bonus: " << wallet << "\n";

    Money expense(5, 25);
    wallet -= expense;
    std::cout << "After -= expense: " << wallet << "\n";

    return 0;
}
./MoneyAdvanced
$ ./MoneyAdvanced
Initial wallet: $50.75
Debt (negated): $-50.-75
Empty wallet evaluates to true with !
After ++wallet: $51.75
After wallet++ (old): $51.75, current: $52.75
After --wallet: $51.75
After += bonus: $62.25
After -= expense: $56.100
Execution finished with exit code 0.

Тепер клас Money підтримує повний набір операторів: унарні, інкремент/декремент, складені присвоювання. Код виглядає природно і читабельно.


Практичне завдання: клас Matrix2x2

Закріпимо матеріал, реалізувавши клас Matrix2x2 для матриць 2×2 із повним набором операторів.

Вимоги:

  • Поля: масив data[2][2]
  • Оператори: +, - (бінарні), - (унарний), * (множення на скаляр), [] (індексація рядків)
  • Оператори присвоювання: +=, -=
  • Вивід: operator<<

Резюме: розширений арсенал операторів

У цій статті ми вивчили перевантаження операторів через методи класу — категорію операторів, що тісно пов'язані з об'єктом і природно реалізуються як його методи.

➖ Унарні оператори

-, !, ~ — const-методи без параметрів. Повертають новий об'єкт (для -) або bool (для !). Використовуються для негації, інверсії, перевірки «порожності».

⏫ Інкремент/декремент

++, -- — дві форми: префіксна (без параметрів, повертає *this) та постфіксна (фіктивний int, повертає копію). Префіксна ефективніша через відсутність копіювання.

🔢 Індексація

operator[] — дві версії: не-const (повертає Type&, дозволяє запис) та const (повертає const Type&, лише читання). Обов'язково метод класу. Дозволяє синтаксис array[i].

📞 Виклик функції

operator() — функтори, об'єкти-функції. Зберігають стан між викликами. Використовуються у алгоритмах STL, є основою для лямбда-виразів C++11.

➕= Складені присвоювання

+=, -=, *=, /= — модифікують об'єкт, повертають *this для ланцюгових операцій. Базовий operator= вимагає перевірки самоприсвоювання для класів із динамічною пам'яттю.

Анонс наступної статті

У цій статті ми завершили вивчення базових та розширених операторів, що роблять користувацькі класи природними у використанні. Але є ще одна категорія операторів, що заслуговує на окрему статтю: приведення типів та спеціальні механізми контролю.

У наступній статті (стаття 65) ми розглянемо:

  • Оператори приведення типів (operator int(), operator double()) — автоматична конвертація класу в інші типи
  • Ключове слово explicit — заборона неявних конвертацій, захист від тонких помилок
  • Ключове слово delete — явна заборона викликів конструкторів або операторів
  • Конструктори конвертації — як конструктори з одним параметром стають операторами перетворення

Ці механізми критично важливі для створення безпечних класів, що захищені від випадкових неявних конвертацій — одного з найнебезпечніших джерел помилок у C++.

Практична порада: після реалізації операторів для вашого класу, протестуйте крайові випадки: самоприсвоювання (a = a), ділення на нуль, вихід за межі індексу, переповнення. Використовуйте assert() для перевірки передумов у Debug-режимі. Це допоможе виявити помилки на етапі розробки, а не у продакшені.
Copyright © 2026