C++

Функції: перевантаження та шаблони

Перевантаження функцій (function overloading) у C++. Правила розв

Одне ім'я — різна поведінка

Функція print могла б виводити ціле число, дійсне, символ або рядок. Без спеціальних механізмів доводилося б придумувати унікальне ім'я для кожного варіанту:

void printInt(int value)    { cout << value; }
void printDouble(double value) { cout << value; }
void printChar(char value)  { cout << value; }

Це незручно: читач коду мусить пам'ятати різні імена для одної концептуальної дії. C++ пропонує дві елегантні альтернативи: перевантаження функцій та шаблони функцій. Обидва дозволяють одному імені функції служити різним типам — але реалізують це принципово різними способами.

Перевантаження функцій (Function Overloading)

Ідея та синтаксис

Перевантаження функцій — оголошення кількох функцій з однаковим іменем, але різними списками параметрів. Компілятор сам визначає, яку саме функцію викликати, виходячи з типів та кількості переданих аргументів.

// Три функції з однаковим іменем, але різними параметрами
void print(int value)
{
    cout << "int: " << value << "\n";
}

void print(double value)
{
    cout << "double: " << value << "\n";
}

void print(const char text[])
{
    cout << "string: " << text << "\n";
}

int main()
{
    print(42);         // → print(int)    → "int: 42"
    print(3.14);       // → print(double) → "double: 3.14"
    print("Hello");    // → print(char[]) → "string: Hello"

    return 0;
}

Механізм вибору правильної функції — роздільна здатність перевантаження (overload resolution). Він відбувається виключно на етапі компіляції: до виконання програми компілятор вже знає, яка саме функція буде викликана.

Що вважається різними підписами

Перевантаження визначається сигнатурою функції: ім'ям та списком типів параметрів (без типу повернення!). Дві функції мають різну сигнатуру, якщо відрізняються:

  • Кількістю параметрів
  • Типами параметрів (у будь-якій позиції)
  • Порядком типів параметрів
// ✅ Різні сигнатури — коректне перевантаження
int add(int a, int b);
double add(double a, double b);
int add(int a, int b, int c);         // Три параметри
double add(int a, double b);          // Різні типи

// ❌ НЕ є перевантаженням — однакова сигнатура!
int multiply(int a, int b);
double multiply(int a, int b);  // Різниться лише тип повернення — ПОМИЛКА
Тип повернення не є частиною сигнатури. Дві функції з однаковими параметрами та різними типами повернення — це помилка компіляції. Компілятор не може з'ясувати, яку з них виклика ти, якщо результат не присвоюється змінній конкретного типу.

Правила роздільної здатності перевантаження

Компілятор вибирає перевантажену функцію у кілька кроків — від «ідеального» збігу до «найкращого з можливих»:

Loading diagram...
flowchart TD
    A["Виклик: foo(arg)"] --> B{"Точний збіг\n(exact match)?"}
    B -- "так" --> G["✅ Викликає цю функцію"]
    B -- "ні" --> C{"Збіг після trivial\nconversion (const, &)?"}
    C -- "так" --> G
    C -- "ні" --> D{"Збіг після\nрозширення типу?\n(char→int, float→double)"}
    D -- "так" --> G
    D -- "ні" --> E{"Збіг після\nнеявного перетворення?"}
    E -- "так" --> F{"Збіг лише один?\nЧи кілька однаково підходять?"}
    F -- "один" --> G
    F -- "кілька" --> H["❌ Помилка:\nambiguous call"]
    E -- "ні" --> I["❌ Помилка:\nno matching function"]

    style G fill:#22c55e,stroke:#16a34a,color:#ffffff
    style H fill:#ef4444,stroke:#dc2626,color:#ffffff
    style I fill:#ef4444,stroke:#dc2626,color:#ffffff
void test(int x)    { cout << "int\n"; }
void test(double x) { cout << "double\n"; }
void test(float x)  { cout << "float\n"; }

test(5);     // → test(int)    — точний збіг
test(3.14);  // → test(double) — точний збіг (літерал double)
test(3.14f); // → test(float)  — точний збіг (суфікс f → float)
test('A');   // → test(int)    — char розширюється до int

Неоднозначне перевантаження (Ambiguous Call)

Якщо компілятор знаходить кілька функцій, що однаково добре підходять до виклику — це помилка неоднозначності:

void ambiguous(int a, double b)    { cout << "1\n"; }
void ambiguous(double a, int b)    { cout << "2\n"; }

ambiguous(1, 2);  // ❌ Помилка! Обидві вимагають однакових перетворень:
                  // 1→double + 2→int  vs  1→int + 2→double → рівнозначні

Рішення — або явно вказати тип через static_cast, або переглянути дизайн функцій:

ambiguous(1, static_cast<double>(2));  // → ambiguous(int, double)

Перевантаження по кількості параметрів

// Пошук максимуму серед 2, 3 або 4 чисел
int max(int a, int b)
{
    return (a > b) ? a : b;
}

int max(int a, int b, int c)
{
    return max(max(a, b), c);  // Перевикористовує двопараметровий max!
}

int max(int a, int b, int c, int d)
{
    return max(max(a, b), max(c, d));
}

int main()
{
    cout << max(3, 7)          << "\n";  // 7
    cout << max(3, 7, 5)       << "\n";  // 7
    cout << max(3, 7, 5, 9)    << "\n";  // 9
    return 0;
}

Зверніть на рядок 10: max(int, int, int) повторно використовує max(int, int). Не дублює логіку «порівняння двох» — делегує її вже існуючій функції. Це ключовий принцип хорошого проектування.

Перевантаження та значення за замовчуванням

Поєднання перевантаження та default parameters може призвести до неоднозначності:

void greet(const char name[], int times = 1)
{
    for (int i = 0; i < times; i++) cout << "Hi, " << name << "\n";
}

void greet(const char name[])  // ❌ Конфліктує з попередньою!
{
    cout << "Hello, " << name << "\n";
}

greet("Alice");  // Яку функцію викликати? Обидві підходять!
Не поєднуйте перевантаження та параметри за замовчуванням, якщо вони можуть утворити неоднозначні виклики. Це одне з найпоширеніших джерел несподіваних помилок компіляції.

Шаблони функцій (Function Templates)

Проблема: дублювання для різних типів

Перевантаження вирішує проблему «одне ім'я, різні типи», але вимагає писати окреме тіло для кожного типу. Якщо логіка ідентична — це дублювання:

int    absVal(int x)    { return (x < 0) ? -x : x; }
double absVal(double x) { return (x < 0) ? -x : x; }
float  absVal(float x)  { return (x < 0) ? -x : x; }
// Три функції. Одна логіка. Тричі.

Шаблони функцій (function templates) вирішують це «раз і назавжди»: ми описуємо логіку один раз, параметризуючи тип. Компілятор автоматично генерує потрібні версії під конкретні типи при кожному виклику.

Синтаксис шаблону

template<typename T>
T absVal(T x)
{
    return (x < 0) ? -x : x;
}

Розберемо синтаксис:

  • template<typename T> — оголошення шаблону. Tпараметр типу (type parameter): заповнювач для конкретного типу. Ім'я T — умовне; можна написати Type, ValueType тощо, але T — загальноприйнята домовленість.
  • T absVal(T x) — функція, де тип повернення і тип параметра — однакові (T). Компілятор підставить реальний тип замість T.

Інстанціація шаблону

Коли компілятор зустрічає виклик шаблонної функції з конкретним типом — він інстанціює (instantiates) шаблон: генерує повноцінну функцію, підставляючи конкретний тип замість T.

template<typename T>
T absVal(T x)
{
    return (x < 0) ? -x : x;
}

int main()
{
    cout << absVal(-5)    << "\n";   // T = int    → компілятор генерує absVal<int>
    cout << absVal(-3.14) << "\n";   // T = double → компілятор генерує absVal<double>
    cout << absVal(-2.5f) << "\n";   // T = float  → компілятор генерує absVal<float>

    return 0;
}

Компілятор сам виводить тип T із типу переданого аргументу — це неявна інстанціація (implicit instantiation). Але можна вказати тип і явно:

cout << absVal<int>(-5)     << "\n";  // Явна інстанціація: T = int
cout << absVal<double>(-5)  << "\n";  // Явна: T = double (int → double)

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

Шаблони з кількома типами

Шаблони можуть мати кілька параметрів типів:

template<typename T, typename U>
void printPair(T first, U second)
{
    cout << "(" << first << ", " << second << ")\n";
}

int main()
{
    printPair(1, 3.14);       // T=int, U=double
    printPair("age", 25);     // T=char[], U=int
    printPair(true, 'x');     // T=bool, U=char
    return 0;
}

Шаблон функції обміну (swap)

Один із найкласичніших шаблонів — універсальний обмін двох значень. Стандартна бібліотека C++ (STL) містить std::swap — саме як шаблон:

template<typename T>
void swapValues(T& a, T& b)   // T& — посилання, щоб змінювати оригінали
{
    T temp = a;
    a = b;
    b = temp;
}

int main()
{
    int x = 5, y = 10;
    swapValues(x, y);
    cout << x << " " << y << "\n";  // 10 5

    double p = 1.1, q = 2.2;
    swapValues(p, q);
    cout << p << " " << q << "\n";  // 2.2 1.1

    return 0;
}

Тут T& — посилання на тип T. Без & обмін відбувався б з копіями, і оригінали не змінились bi. Посилання детально розглядаються у окремому розділі.

Шаблон з не-типовим параметром

Параметр шаблону може бути не лише типом, а й конкретним значенням (константою):

// N — розмір масиву, визначений на етапі компіляції
template<typename T, int N>
void printArray(T arr[N])
{
    for (int i = 0; i < N; i++)
    {
        cout << arr[i] << " ";
    }
    cout << "\n";
}

int main()
{
    int nums[5] = {1, 2, 3, 4, 5};
    printArray<int, 5>(nums);   // T=int, N=5

    return 0;
}

Спеціалізація шаблону

Іноді загальна логіка шаблону не підходить для конкретного типу. Спеціалізація (template specialization) дозволяє перевизначити поведінку шаблону для окремого типу:

// Загальний шаблон
template<typename T>
T getMax(T a, T b)
{
    return (a > b) ? a : b;
}

// Спеціалізація для const char* (рядки порівнюємо не через >)
template<>
const char* getMax<const char*>(const char* a, const char* b)
{
    return (strcmp(a, b) > 0) ? a : b;
}

int main()
{
    cout << getMax(3, 7)              << "\n";  // 7 (загальний шаблон)
    cout << getMax("apple", "banana") << "\n";  // banana (спеціалізація)
    return 0;
}

Overloading vs Templates: коли що обирати?

Обидва механізми вирішують схожу задачу. Ключова відмінність — у природі відмінностей між варіантами:

OverloadingTemplate
ЛогікаМоже відрізнятися для кожного типуОднакова для всіх типів
Кількість кодуОкреме тіло для кожного типуОдне тіло для всіх типів
Нові типиПотребує нового перевантаженняАвтоматично підтримує все
ПомилкиНа рівні виклику функціїНа рівні шаблону (іноді складні)
ГнучкістьПовна — кожна версія робить своєОбмежена — логіка фіксована
// Overloading — різна логіка для різних типів
void serialize(int value)     { cout << value; }
void serialize(double value)  { cout << fixed << value; }
void serialize(bool value)    { cout << (value ? "true" : "false"); }

// Template — однакова логіка, будь-який тип
template<typename T>
T clamp(T value, T minVal, T maxVal)
{
    if (value < minVal) return minVal;
    if (value > maxVal) return maxVal;
    return value;
}

serialize — ідеальний кандидат для перевантаження: кожен тип серіалізується по-своєму. clamp — ідеальний шаблон: «затискання» значення між мінімумом і максимумом однаково для int, double, float.

Поєднання: overloading + templates

Шаблон і перевантаження можуть існувати разом. Компілятор спочатку шукає точний збіг серед перевантажень — і лише потім вдається до шаблону:

template<typename T>
void describe(T value)
{
    cout << "Generic: " << value << "\n";
}

// Явне перевантаження для bool
void describe(bool value)
{
    cout << "Bool: " << (value ? "true" : "false") << "\n";
}

int main()
{
    describe(42);      // → шаблон (Generic: 42)
    describe(3.14);    // → шаблон (Generic: 3.14)
    describe(true);    // → перевантаження! (Bool: true)
    return 0;
}

Повний приклад: Типобезпечна математична бібліотека

MathLib.cpp
#include <iostream>
#include <cmath>

using namespace std;

// ─── Шаблони ───────────────────────────────────────────────

template<typename T>
T getMin(T a, T b)
{
    return (a < b) ? a : b;
}

template<typename T>
T getMax(T a, T b)
{
    return (a > b) ? a : b;
}

template<typename T>
T clamp(T value, T lo, T hi)
{
    return getMax(lo, getMin(value, hi));
}

template<typename T>
T absVal(T x)
{
    return (x < 0) ? -x : x;
}

// ─── Перевантаження ────────────────────────────────────────

// round для int — вже ціле, нічого не змінюємо
int roundTo(int value, int /* decimals */)
{
    return value;
}

// round для double — реальне округлення
double roundTo(double value, int decimals)
{
    double factor = pow(10.0, decimals);
    return round(value * factor) / factor;
}

// ─── main ──────────────────────────────────────────────────

int main()
{
    // Шаблони — один код, різні типи
    cout << getMin(3, 7)          << "\n";   // 3 (int)
    cout << getMin(3.14, 2.72)    << "\n";   // 2.72 (double)

    cout << clamp(150, 0, 100)    << "\n";   // 100
    cout << clamp(-5.0, 0.0, 1.0) << "\n";  // 0

    // Перевантаження — різна логіка
    cout << roundTo(42, 2)        << "\n";   // 42 (int версія)
    cout << roundTo(3.14159, 2)   << "\n";   // 3.14 (double версія)

    return 0;
}

Результат:

3
2.72
100
0
42
3.14

getMin, getMax, clamp, absValшаблони: однакова логіка, будь-який тип. roundToперевантаження: для int і double різна реалізація.


Практичні завдання

Рівень 1 — Базовий

Рівень 2 — Логічний

Рівень 3 — Творчий

Підсумок

📌 Overloading

Кілька функцій з однаковим іменем, різними параметрами. Компілятор обирає на етапі компіляції. Тип повернення — не частина сигнатури. Використовуйте, коли логіка різна для різних типів.

📌 Роздільна здатність

Компілятор шукає: точний збіг → розширення типу → неявне перетворення. Якщо кілька варіантів рівнозначні — помилка ambiguous call.

📌 Template

template<typename T> — параметризує тип. Одна логіка для всіх типів. Інстанціація — автоматична (неявна) або явна <int>. Використовуйте, коли логіка однакова.

📌 Спеціалізація

template<> void f<SpecificType>(...) — перевизначення шаблону для конкретного типу. Дозволяє зберегти загальний шаблон, але «виправити» його для особливих випадків.

📌 Overloading + Templates

Можна поєднувати. Перевантаження має пріоритет над шаблоном при точному збігу. Шаблон — «запасний варіант» для інших типів.
Copyright © 2026