C++

Перевантаження операторів: основи та арифметика

Як зробити користувацькі класи природними у використанні через перевантаження операторів. Арифметичні оператори (+, -, *, /), оператори порівняння, вводу/виводу. Friend-функції vs методи класу.

Перевантаження операторів: основи та арифметика

Проблема: класи поводяться «не так, як треба»

Одна з найбільших переваг вбудованих типів C++ (int, double, bool) — вони поводяться інтуїтивно. Ми пишемо a + b, x < y, count++ — і все працює так, як очікуємо. Оператори — це частина природної мови програмування, що робить код читабельним і виразним.

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

MoneyProblem.cpp
#include <iostream>

class Money
{
private:
    int dollars;
    int cents;

public:
    Money(int d, int c) : dollars(d), cents(c) {}

    int getDollars() const { return dollars; }
    int getCents() const { return cents; }
};

int main()
{
    Money wallet(50, 75);   // $50.75
    Money tip(10, 25);      // $10.25

    // ❌ Хочемо написати:
    // Money total = wallet + tip;

    // ✅ Але змушені робити так:
    Money total(
        wallet.getDollars() + tip.getDollars(),
        wallet.getCents() + tip.getCents()
    );

    std::cout << "Total: $" << total.getDollars() << "." << total.getCents() << "\n";

    return 0;
}

Рядки 27–30 — це те, що ми змушені писати, бо компілятор не знає, як додавати об'єкти Money. Але це виглядає багатослівно та неприродньо. Ми хочемо писати wallet + tip, як для чисел. Ми хочемо писати wallet < tip для порівняння, std::cout << wallet для виводу.

Перевантаження операторів (operator overloading) дозволяє навчити компілятор розуміти звичайні оператори для користувацьких типів. Після перевантаження ми зможемо писати:

Money total = wallet + tip;
if (wallet > tip) { /* ... */ }
std::cout << wallet;

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

Перевантаження оператора (operator overloading) — це механізм, що дозволяє визначити поведінку стандартних операторів (+, -, *, ==, << тощо) для користувацьких типів. Компілятор перетворює вираз a + b на виклик функції operator+(a, b) або a.operator+(b), залежно від того, як оператор перевантажений.

Оператори як функції: що насправді відбувається

У C++ оператори — це синтаксичний цукор для функцій зі спеціальними іменами. Коли компілятор зустрічає вираз a + b, він шукає функцію з іменем operator+, що приймає типи операндів a і b.

Є два способи перевантажити оператор:

1. Через вільну функцію (або friend-функцію):

ReturnType operator+(const Type& left, const Type& right);

Вираз a + b перетворюється на виклик operator+(a, b).

2. Через метод класу:

class Type
{
    ReturnType operator+(const Type& right) const;
};

Вираз a + b перетворюється на виклик a.operator+(b). Лівий операнд стає неявним об'єктом this.

Розглянемо конкретний приклад із числами:

OperatorAsSyntaxSugar.cpp
#include <iostream>

int main()
{
    int a = 10, b = 5;

    // Спосіб 1: природний синтаксис
    int sum1 = a + b;

    // Спосіб 2: явний виклик функції operator+ (не працює для вбудованих типів)
    // int sum2 = operator+(a, b);  // ❌ ПОМИЛКА: немає такої функції

    std::cout << "Sum: " << sum1 << "\n";

    // Але для користувацьких типів компілятор ДІЙСНО викликає operator+
    // Money total = wallet + tip;
    // перетворюється на:
    // Money total = operator+(wallet, tip);  або  wallet.operator+(tip);

    return 0;
}

Для вбудованих типів (int, double) оператори жорстко закодовані у компілятор — їх не можна перевантажити. Але для користувацьких типів ми визначаємо поведінку оператора, створюючи функцію з іменем operator+.

Не можна змінювати поведінку операторів для вбудованих типів. Вираз int operator+(int a, int b) { return a - b; } — недопустимий. Оператори можна перевантажувати лише тоді, коли хоча б один операнд є користувацьким типом.

Фундаментальні правила перевантаження

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

🚫 Не можна створювати нові оператори

Ви не можете винайти оператор ** для піднесення до степеня або <=> (хоча останній з'явився в C++20 як вбудований). Лише існуючі оператори можна перевантажувати.

🚫 Не можна змінювати арність

Арність (arity) — кількість операндів. Оператор + завжди бінарний (два операнди), - може бути унарним (один операнд) або бінарним. Ви не можете зробити + унарним або ! бінарним.

🚫 Не можна змінювати пріоритет

Пріоритет операторів фіксований: * виконується раніше за +, && раніше за ||. Перевантаження не змінює порядок обчислення виразів.

🚫 Деякі оператори заборонені

Не можна перевантажити:

  • :: (scope resolution)
  • . (member access)
  • .* (pointer-to-member selection)
  • ?: (ternary conditional)
  • sizeof, typeid, alignof

Ці оператори критичні для роботи компілятора і мови.

Принцип найменшого здивування (Principle of Least Astonishment): перевантажуйте оператори так, щоб їхня поведінка відповідала інтуїції. Оператор + має додавати, < — порівнювати на «менше», << — виводити у потік. Якщо money1 + money2 віднімає гроші — це порушення принципу. Код стає заплутаним і небезпечним.

Перший приклад: додавання грошей

Почнемо з найпростішого оператора — бінарного +. Ми хочемо, щоб вираз wallet + tip повертав новий об'єкт Money із сумою.

Крок 1: Базовий клас Money

Спочатку реалізуємо клас без перевантаження, але з коректною логікою додавання центів (врахування переповнення на долари):

MoneyBasic.cpp
#include <iostream>

class Money
{
private:
    int dollars;
    int cents;

    // Нормалізація: якщо центів >= 100, переносимо у долари
    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 getDollars() const { return dollars; }
    int getCents() const { return cents; }

    // Допоміжний метод для виводу
    void print() const
    {
        std::cout << "$" << dollars << ".";
        if (cents < 10)
        {
            std::cout << "0";  // Доповнення нулем: $5.05 замість $5.5
        }
        std::cout << cents;
    }
};

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

    std::cout << "Wallet: ";
    wallet.print();
    std::cout << "\nTip: ";
    tip.print();
    std::cout << "\n";

    // Ручне додавання — багатослівно
    Money total(wallet.getDollars() + tip.getDollars(),
                wallet.getCents() + tip.getCents());

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

    return 0;
}
./MoneyBasic
$ ./MoneyBasic
Wallet: $50.75
Tip: $10.50
Total: $61.25
Execution finished with exit code 0.

Рядки 10–23: метод normalize() обробляє переповнення центів. Якщо cents >= 100, конвертуємо у долари. Це гарантує, що Money(5, 150) перетвориться на $6.50.

Рядки 59–60: ручне додавання через геттери — працює, але код важкий для читання. Давайте додамо оператор +.

Крок 2: Перевантаження через friend-функцію

Оператор + природно реалізувати як вільну функцію (або friend-функцію), бо обидва операнди мають рівну роль — не має сенсу вважати лівий операнд «головним».

MoneyOperatorPlus.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 getDollars() const { return dollars; }
    int getCents() const { return cents; }

    void print() const
    {
        std::cout << "$" << dollars << ".";
        if (cents < 10) std::cout << "0";
        std::cout << cents;
    }

    // Оголошуємо operator+ дружньою функцією
    friend Money operator+(const Money& left, const Money& right);
};

// Визначення operator+ — поза класом
Money operator+(const Money& left, const Money& right)
{
    // Додаємо долари до доларів, центи до центів
    // Конструктор автоматично нормалізує результат
    return Money(left.dollars + right.dollars,
                 left.cents + right.cents);
}

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

    std::cout << "Wallet: ";
    wallet.print();
    std::cout << "\nTip: ";
    tip.print();
    std::cout << "\n";

    // ✅ Тепер можемо писати природно!
    Money total = wallet + tip;

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

    // Можна навіть ланцюгово
    Money extra(5, 0);
    Money grandTotal = wallet + tip + extra;

    std::cout << "Grand Total: ";
    grandTotal.print();
    std::cout << "\n";

    return 0;
}
./MoneyOperatorPlus
$ ./MoneyOperatorPlus
Wallet: $50.75
Tip: $10.50
Total: $61.25
Grand Total: $66.25
Execution finished with exit code 0.

Рядок 41: оголошення friend Money operator+(...) всередині класу надає функції доступ до приватних полів dollars і cents.

Рядки 45–51: визначення функції поза класом. Вона створює новий об'єкт Money через конструктор, який автоматично нормалізує центи.

Рядок 66: wallet + tip — компілятор перетворює це на operator+(wallet, tip). Код читається природно.

Рядок 71: ланцюговий виклик wallet + tip + extra працює завдяки асоціативності зліва направо: спочатку (wallet + tip) повертає тимчасовий Money, потім temp + extra.

Чому friend, а не звичайна вільна функція? Без friend функція operator+ не мала б доступу до left.dollars і right.cents — вони приватні. Довелося б використовувати геттери, що робить код багатослівнішим: return Money(left.getDollars() + right.getDollars(), ...). friend дозволяє писати лаконічно.

Арифметичні оператори: повний набір

Тепер, коли ми зрозуміли принцип, додамо решту арифметичних операторів: -, *, /. Логіка аналогічна до +.

MoneyArithmetic.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 getDollars() const { return dollars; }
    int getCents() const { return cents; }

    // Для множення та ділення корисно отримати загальну кількість центів
    int totalCents() const
    {
        return dollars * 100 + cents;
    }

    void print() const
    {
        std::cout << "$" << dollars << ".";
        if (cents < 10) std::cout << "0";
        std::cout << cents;
    }

    // Оголошуємо всі арифметичні оператори дружніми
    friend Money operator+(const Money& left, const Money& right);
    friend Money operator-(const Money& left, const Money& right);
    friend Money operator*(const Money& money, int multiplier);
    friend Money operator/(const Money& money, int divisor);
};

Money operator+(const Money& left, const Money& right)
{
    return Money(left.dollars + right.dollars,
                 left.cents + right.cents);
}

Money operator-(const Money& left, const Money& right)
{
    return Money(left.dollars - right.dollars,
                 left.cents - right.cents);
}

// Множимо гроші на ціле число
Money operator*(const Money& money, int multiplier)
{
    int totalCents = money.totalCents();
    int resultCents = totalCents * multiplier;
    return Money(resultCents / 100, resultCents % 100);
}

// Ділимо гроші на ціле число
Money operator/(const Money& money, int divisor)
{
    if (divisor == 0)
    {
        std::cerr << "Error: Division by zero\n";
        return Money(0, 0);
    }

    int totalCents = money.totalCents();
    int resultCents = totalCents / divisor;
    return Money(resultCents / 100, resultCents % 100);
}

int main()
{
    Money price(15, 99);    // $15.99
    Money discount(3, 50);  // $3.50

    std::cout << "Price: ";
    price.print();
    std::cout << "\nDiscount: ";
    discount.print();
    std::cout << "\n\n";

    Money final = price - discount;
    std::cout << "After discount: ";
    final.print();
    std::cout << "\n";

    Money doubled = price * 2;
    std::cout << "Doubled price: ";
    doubled.print();
    std::cout << "\n";

    Money half = price / 2;
    std::cout << "Half price: ";
    half.print();
    std::cout << "\n";

    // Комбіновані вирази
    Money combo = (price - discount) * 3;
    std::cout << "Three items after discount: ";
    combo.print();
    std::cout << "\n";

    return 0;
}
./MoneyArithmetic
$ ./MoneyArithmetic
Price: $15.99
Discount: $3.50
After discount: $12.49
Doubled price: $31.98
Half price: $7.99
Three items after discount: $37.47
Execution finished with exit code 0.

Рядки 34–37: метод totalCents() конвертує гроші у центи — корисно для множення та ділення, щоб уникнути втрати точності.

Рядки 66–70: множення Money * int. Конвертуємо у центи, множимо, конвертуємо назад у долари та центи.

Рядки 73–84: ділення Money / int. Перевіряємо ділення на нуль, потім аналогічно множенню.

Рядок 117: комбінований вираз (price - discount) * 3 — демонструє, що оператори працюють разом природно, як для вбудованих типів.

Втрата точності при діленні. Ділення $10.00 / 3 дає $3.33, втрачаючи один цент через цілочисельне ділення. У реальних фінансових системах використовують спеціалізовані бібліотеки для роботи з грошима (наприклад, зберігають кількість найменших одиниць валюти — центів).

Симетрія операторів: money * 2 vs 2 * money

Поточна реалізація operator* працює для price * 2, але не працює для 2 * price. Спробуємо:

Money doubled1 = price * 2;   // ✅ Працює: operator*(price, 2)
Money doubled2 = 2 * price;   // ❌ ПОМИЛКА: немає operator*(int, Money)

Щоб забезпечити комутативність (симетрію) множення, потрібно додати другу версію оператора:

MoneyCommutative.cpp
class Money
{
    // ... попередній код ...

    friend Money operator*(const Money& money, int multiplier);
    friend Money operator*(int multiplier, const Money& money);  // Симетрична версія
};

Money operator*(const Money& money, int multiplier)
{
    int totalCents = money.totalCents();
    int resultCents = totalCents * multiplier;
    return Money(resultCents / 100, resultCents % 100);
}

// Симетрична версія: делегує роботу першій
Money operator*(int multiplier, const Money& money)
{
    return money * multiplier;  // Викликає operator*(Money, int)
}

int main()
{
    Money price(10, 50);

    Money doubled1 = price * 3;   // ✅ Працює
    Money doubled2 = 3 * price;   // ✅ Тепер теж працює!

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

    return 0;
}

Рядки 17–20: друга версія operator* приймає int зліва, Money справа. Вона просто викликає першу версію — уникаємо дублювання коду.

Тепер обидва варіанти працюють:

  • price * 3operator*(price, 3) (перша версія)
  • 3 * priceoperator*(3, price) (друга версія) → викликає першу
Коли потрібна симетрія? Для комутативних операцій (множення, додавання) симетрія природна: a + b = b + a. Для некомутативних (віднімання, ділення) симетрія не має сенсу: money - 55 - money. Множення грошей на число комутативне, тому симетричні версії корисні.

Оператори порівняння: ==, !=, <, <=, >, >=

Оператори порівняння дозволяють порівнювати об'єкти:

if (wallet > tip) { /* більше грошей у гаманці */ }
if (price == discount) { /* однакова сума */ }

Реалізуємо всі шість операторів. Трюк: достатньо реалізувати == та <, а решту виразити через них:

MoneyComparison.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;
    }

    void print() const
    {
        std::cout << "$" << dollars << ".";
        if (cents < 10) std::cout << "0";
        std::cout << cents;
    }

    // Оператори порівняння
    friend bool operator==(const Money& left, const Money& right);
    friend bool operator!=(const Money& left, const Money& right);
    friend bool operator<(const Money& left, const Money& right);
    friend bool operator<=(const Money& left, const Money& right);
    friend bool operator>(const Money& left, const Money& right);
    friend bool operator>=(const Money& left, const Money& right);
};

// Базові оператори: == та <
bool operator==(const Money& left, const Money& right)
{
    return left.totalCents() == right.totalCents();
}

bool operator<(const Money& left, const Money& right)
{
    return left.totalCents() < right.totalCents();
}

// Решта операторів виражаються через == та <
bool operator!=(const Money& left, const Money& right)
{
    return !(left == right);
}

bool operator<=(const Money& left, const Money& right)
{
    return (left < right) || (left == right);
}

bool operator>(const Money& left, const Money& right)
{
    return !(left <= right);
}

bool operator>=(const Money& left, const Money& right)
{
    return !(left < right);
}

int main()
{
    Money wallet(50, 0);
    Money tip(25, 50);
    Money another(50, 0);

    std::cout << "Wallet: ";
    wallet.print();
    std::cout << "\nTip: ";
    tip.print();
    std::cout << "\nAnother: ";
    another.print();
    std::cout << "\n\n";

    // Тестуємо всі оператори
    std::cout << "wallet == another: " << (wallet == another) << "\n";
    std::cout << "wallet != tip: " << (wallet != tip) << "\n";
    std::cout << "wallet > tip: " << (wallet > tip) << "\n";
    std::cout << "tip < wallet: " << (tip < wallet) << "\n";
    std::cout << "wallet >= another: " << (wallet >= another) << "\n";
    std::cout << "tip <= wallet: " << (tip <= wallet) << "\n";

    // Використання у умовах
    if (wallet > tip)
    {
        std::cout << "\nWallet has more money than tip\n";
    }

    return 0;
}
./MoneyComparison
$ ./MoneyComparison
Wallet: $50.00
Tip: $25.50
Another: $50.00
wallet == another: 1
wallet != tip: 1
wallet > tip: 1
tip < wallet: 1
wallet >= another: 1
tip <= wallet: 1
Wallet has more money than tip
Execution finished with exit code 0.

Рядки 52–59: базові оператори == та < порівнюють загальну кількість центів. Це гарантує коректне порівняння незалежно від того, як зберігаються долари та центи.

Рядки 62–79: решта чотирьох операторів виражаються через == та <. Наприклад:

  • a != b!(a == b)
  • a <= b(a < b) || (a == b)
  • a > b!(a <= b)
  • a >= b!(a < b)

Це зменшує дублювання коду та ризик помилок.

C++20 spaceship operator (<=>): У C++20 з'явився оператор тристороннього порівняння <=> (spaceship operator), що автоматично генерує всі шість операторів порівняння з одного визначення. Але у цьому курсі ми дотримуємося C++11/14/17 стандартів, тому реалізуємо їх вручну.

Оператори вводу/виводу: << та >>

Найкорисніші оператори для користувацьких класів — operator<< (вивід у потік) та operator>> (ввід із потоку). Вони дозволяють використовувати об'єкти з std::cout та std::cin природно:

std::cout << money;  // Замість money.print()
std::cin >> money;   // Замість ручного вводу через геттери

Критично важливо: оператори << та >> завжди перевантажуються як вільні (або friend) функції, бо лівий операнд — це потік (std::ostream& або std::istream&), а не наш клас. Ми не можемо додати методи до стандартних класів потоків.

MoneyIO.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();
    }

    // Оператори вводу/виводу
    friend std::ostream& operator<<(std::ostream& out, const Money& money);
    friend std::istream& operator>>(std::istream& in, 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;  // Повертаємо потік для ланцюгових викликів
}

// Ввід із потоку
std::istream& operator>>(std::istream& in, Money& money)
{
    std::cout << "Enter dollars: ";
    in >> money.dollars;
    std::cout << "Enter cents: ";
    in >> money.cents;
    money.normalize();  // Нормалізуємо після вводу
    return in;  // Повертаємо потік для ланцюгових викликів
}

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

    // Вивід через operator<<
    std::cout << "Wallet: " << wallet << "\n";

    // Ввід через operator>>
    std::cout << "Enter tip amount:\n";
    std::cin >> tip;

    // Ланцюговий вивід
    std::cout << "Wallet: " << wallet << ", Tip: " << tip << "\n";

    Money total = wallet + tip;
    std::cout << "Total: " << total << "\n";

    return 0;
}
./MoneyIO
$ ./MoneyIO
Wallet: $50.75
Enter tip amount:
Enter dollars: 10
Enter cents: 50
Wallet: $50.75, Tip: $10.50
Total: $61.25
Execution finished with exit code 0.

Рядок 36–44: operator<< приймає std::ostream& (посилання на потік виводу) та const Money& (об'єкт для виводу). Виводить гроші у форматі $XX.XX і повертає потік, щоб дозволити ланцюгові виклики.

Рядок 44: return out; критично важливий! Він дозволяє писати std::cout << wallet << ", Tip: " << tip. Компілятор перетворює це на:

operator<<(operator<<(std::cout, wallet), ", Tip: ")...

Рядки 48–56: operator>> приймає std::istream& (посилання на потік вводу) та не-const Money& (об'єкт буде змінено). Зчитує долари та центи, нормалізує, повертає потік.

Чому Money&, а не const Money& у operator>>? Оператор вводу змінює об'єкт, тому параметр не може бути константним. Натомість operator<< не змінює об'єкт, тому використовує const Money&.

Friend-функція vs метод класу: коли що використовувати

Ми бачили, що оператори можна перевантажувати як friend-функції або як методи класу. Коли використовувати який підхід?

Через friend-функцію (або вільну функцію):

friend ReturnType operator@(const Type& left, const Type& right);

Переваги:

  • Симетрія: обидва операнди мають однакову роль
  • Можливість підтримки 2 * money (лівий операнд — не об'єкт класу)
  • Обов'язково для operator<< та operator>> (лівий операнд — потік)

Використання: бінарні арифметичні оператори (+, -, *, /), оператори порівняння (==, <), оператори потоків (<<, >>).

Через метод класу:

class Type
{
    ReturnType operator@(const Type& right) const;
};

Переваги:

  • Коротший синтаксис (немає friend)
  • Лівий операнд — завжди об'єкт класу (неявний this)
  • Обов'язково для операторів присвоювання (=, +=, -=), індексації ([]), виклику (()), доступу до членів (->)

Використання: оператори, що модифікують лівий операнд (присвоювання, інкремент), оператори, де лівий операнд є головним (індексація, виклик).

✅ Через friend

  • Бінарні арифметичні: +, -, *, /
  • Порівняння: ==, !=, <, <=, >, >=
  • Потоки: <<, >>
  • Логічні: &&, ||

✅ Через метод

  • Присвоювання: =, +=, -=, *=, /=
  • Унарні: -, !, ~, ++, --
  • Доступ: [], (), ->
  • Приведення типів
Загальне правило: якщо оператор не змінює об'єкт і має симетричну роль операндів — використовуйте friend-функцію. Якщо оператор змінює лівий операнд або є унарним — використовуйте метод класу. Для операторів потоків (<<, >>) вибору немає — завжди friend-функція.

Повна реалізація класу Money з усіма операторами

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

MoneyComplete.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;
    }

    // Арифметичні оператори
    friend Money operator+(const Money& left, const Money& right);
    friend Money operator-(const Money& left, const Money& right);
    friend Money operator*(const Money& money, int multiplier);
    friend Money operator*(int multiplier, const Money& money);
    friend Money operator/(const Money& money, int divisor);

    // Оператори порівняння
    friend bool operator==(const Money& left, const Money& right);
    friend bool operator!=(const Money& left, const Money& right);
    friend bool operator<(const Money& left, const Money& right);
    friend bool operator<=(const Money& left, const Money& right);
    friend bool operator>(const Money& left, const Money& right);
    friend bool operator>=(const Money& left, const Money& right);

    // Оператори вводу/виводу
    friend std::ostream& operator<<(std::ostream& out, const Money& money);
    friend std::istream& operator>>(std::istream& in, Money& money);
};

// === Реалізація арифметичних операторів ===

Money operator+(const Money& left, const Money& right)
{
    return Money(left.dollars + right.dollars,
                 left.cents + right.cents);
}

Money operator-(const Money& left, const Money& right)
{
    return Money(left.dollars - right.dollars,
                 left.cents - right.cents);
}

Money operator*(const Money& money, int multiplier)
{
    int totalCents = money.totalCents();
    return Money((totalCents * multiplier) / 100,
                 (totalCents * multiplier) % 100);
}

Money operator*(int multiplier, const Money& money)
{
    return money * multiplier;
}

Money operator/(const Money& money, int divisor)
{
    if (divisor == 0)
    {
        std::cerr << "Error: Division by zero\n";
        return Money(0, 0);
    }
    int totalCents = money.totalCents();
    return Money((totalCents / divisor) / 100,
                 (totalCents / divisor) % 100);
}

// === Реалізація операторів порівняння ===

bool operator==(const Money& left, const Money& right)
{
    return left.totalCents() == right.totalCents();
}

bool operator!=(const Money& left, const Money& right)
{
    return !(left == right);
}

bool operator<(const Money& left, const Money& right)
{
    return left.totalCents() < right.totalCents();
}

bool operator<=(const Money& left, const Money& right)
{
    return (left < right) || (left == right);
}

bool operator>(const Money& left, const Money& right)
{
    return !(left <= right);
}

bool operator>=(const Money& left, const Money& right)
{
    return !(left < right);
}

// === Реалізація операторів вводу/виводу ===

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

std::istream& operator>>(std::istream& in, Money& money)
{
    std::cout << "Enter dollars: ";
    in >> money.dollars;
    std::cout << "Enter cents: ";
    in >> money.cents;
    money.normalize();
    return in;
}

int main()
{
    Money wallet(100, 50);
    Money expense(35, 75);

    std::cout << "=== Financial Calculator ===\n\n";

    std::cout << "Wallet: " << wallet << "\n";
    std::cout << "Expense: " << expense << "\n\n";

    // Арифметичні операції
    Money remaining = wallet - expense;
    std::cout << "Remaining after expense: " << remaining << "\n";

    Money tripled = expense * 3;
    std::cout << "Expense tripled: " << tripled << "\n";

    Money shared = wallet / 2;
    std::cout << "Wallet split in half: " << shared << "\n\n";

    // Порівняння
    if (wallet > expense)
    {
        std::cout << "You have enough money for the expense\n";
    }

    if (remaining == Money(64, 75))
    {
        std::cout << "Calculation is correct!\n";
    }

    // Складні вирази
    Money total = (wallet - expense) + (expense / 2);
    std::cout << "\nComplex calculation result: " << total << "\n";

    return 0;
}
./MoneyComplete
$ ./MoneyComplete
=== Financial Calculator ===
Wallet: $100.50
Expense: $35.75
Remaining after expense: $64.75
Expense tripled: $107.25
Wallet split in half: $50.25
You have enough money for the expense
Calculation is correct!
Complex calculation result: $82.62
Execution finished with exit code 0.

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


Діаграма взаємодії: як працює перевантаження

Наступна діаграма показує, як компілятор перетворює вираз із перевантаженими операторами на виклики функцій:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

actor Програміст as prog
participant "Компілятор" as comp
participant "operator+" as opPlus
participant "operator<<" as opStream

prog -> comp : wallet + tip
activate comp
comp -> comp : Шукає operator+(Money, Money)
comp -> opPlus : operator+(wallet, tip)
activate opPlus
opPlus -> opPlus : Створює Money(dollars1+dollars2, cents1+cents2)
opPlus --> comp : Повертає новий Money
deactivate opPlus
comp --> prog : Результат: Money(61, 25)
deactivate comp

prog -> comp : std::cout << total
activate comp
comp -> comp : Шукає operator<<(ostream&, Money)
comp -> opStream : operator<<(std::cout, total)
activate opStream
opStream -> opStream : Виводить "$61.25"
opStream --> comp : Повертає std::cout
deactivate opStream
comp --> prog : Ланцюгове виведення
deactivate comp

@enduml

Компілятор перетворює:

  • wallet + tipoperator+(wallet, tip)
  • std::cout << totaloperator<<(std::cout, total)

Це відбувається на етапі компіляції — жодних додаткових накладних витрат під час виконання.


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

Закріпимо матеріал, реалізувавши клас Vector2D для двовимірних векторів із повним набором операторів.

Вимоги:

  • Поля x та y (координати вектора)
  • Оператори: +, -, * (множення на скаляр), ==, !=
  • Оператори вводу/виводу << та >>
  • Метод length() для обчислення довжини вектора

Резюме: природність через перевантаження

Перевантаження операторів — потужний механізм, що дозволяє користувацьким класам інтегруватися у мову C++ природно і виразно. Замість багатослівних викликів методів (wallet.add(tip).print()) ми пишемо інтуїтивні вирази (std::cout << wallet + tip).

➕ Арифметичні оператори

+, -, *, / — реалізуються через friend-функції для симетрії. Повертають новий об'єкт (за значенням), не змінюють операнди. Для комутативних операцій (множення) додають симетричні версії (int * Money).

⚖️ Оператори порівняння

==, !=, <, <=, >, >= — достатньо реалізувати == та <, решту виразити через них. Повертають bool. Використовуються у умовах та сортуванні.

📤 Оператори потоків

<< (вивід), >> (ввід) — завжди через friend-функції, бо лівий операнд — потік (std::ostream& або std::istream&). Повертають потік за посиланням для ланцюгових викликів.

🎯 Принцип дизайну

Принцип найменшого здивування: оператори мають поводитися інтуїтивно. + додає, < порівнює, << виводить. Не використовуйте + для віднімання або < для виводу — це порушує очікування і робить код небезпечним.

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

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

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

  • Унарні оператори (-, !, ~) — оператори з одним операндом
  • Інкремент та декремент (++, --) — префіксна vs постфіксна форми, різниця сигнатур
  • Оператор індексації ([]) — доступ до елементів контейнера, повернення за посиланням
  • Оператор виклику функції (()) — функтори, об'єкти, що поводяться як функції
  • Оператори присвоювання (=, +=, -=) — модифікація об'єкта, повернення *this

Ці оператори дозволяють створювати контейнери, що поводяться як масиви (array[i]), об'єкти, що викликаються як функції (functor(args)), і класи з природною семантикою присвоювання.

Практична порада: перш ніж перевантажувати оператор, поставте собі запитання: «Чи зробить це код читабельнішим?». Якщо money.add(tip) зрозуміліший за money + tip (що малоймовірно для грошей), не перевантажуйте. Але для векторів, матриць, комплексних чисел, грошей, дат — перевантаження робить код природним і виразним. Це баланс між виразністю та відповідальністю: потужний механізм вимагає обережного використання.
Copyright © 2026