C++

Псевдоніми типів (typedef і using)

Дізнайтеся, навіщо потрібні псевдоніми типів у C++, чим відрізняються typedef та using, як спрощувати складні сигнатури та забезпечувати кросплатформність через стандартні типи з <cstdint>.

Псевдоніми типів (typedef і using)

Проблема: коли назва типу стає перешкодою

Розглянемо реалістичну функцію роботи з навчальними даними:

std::vector<std::pair<std::string, int>> loadStudentScores(const std::string& filename);
void printStudentScores(std::vector<std::pair<std::string, int>> scores);
bool saveStudentScores(std::vector<std::pair<std::string, int>> scores, const std::string& path);

Три прототипи — і std::vector<std::pair<std::string, int>> повторюється тричі. Що це означає семантично? Список пар «ім'я студента → оцінка». Але з коду цього не видно — видно лише технічний шум. Тепер уявімо, що треба змінити int на double (оцінки з десятими). Потрібно виправити три рядки — і не пропустити жоден.

Або ще простіший приклад: функція вимірювання часу:

double calculateSpeed(double distanceInMeters, double timeInSeconds);

Тут обидва параметри — double, але вони означають різні фізичні величини. Компілятор не заперечить, якщо ви переплутаєте порядок аргументів — а людина в коді ревью теж може не помітити.

Відповідь на обидві ці проблеми — псевдоніми типів (type aliases).

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

Підхід 1: typedef — класичний синтаксис

Базовий синтаксис

Ключове слово typedef з'явилося ще у мові C і перейшло в C++ без змін. Загальна форма:

typedef існуючийТип новеІм'я;

Найпростіший приклад — псевдоніми для числових типів:

typedef double Seconds;
typedef double Meters;
typedef int StudentId;

Тепер функція швидкості виглядає так:

Speed.cpp
#include <iostream>

typedef double Seconds;
typedef double Meters;

double calculateSpeed(Meters distance, Seconds time)
{
    return distance / time;
}

int main()
{
    Meters dist = 100.0;
    Seconds t = 9.58;

    std::cout << "Швидкість: " << calculateSpeed(dist, t) << " м/с\n";
    return 0;
}
./Speed
$ ./Speed
Швидкість: 10.4384 м/с
Execution finished with exit code 0.

Сигнатура calculateSpeed(Meters distance, Seconds time) тепер самодокументована: читач одразу розуміє, що перший аргумент — відстань у метрах, другий — час у секундах. Компілятор, щоправда, все одно прийме calculateSpeed(t, dist) без помилки — бо обидва псевдоніми посилаються на double. Але людський намір у коді стає явним.

Порядок слів у typedef: читати праворуч

Найпоширеніша плутанина з typedef — порядок слів. Запам'ятайте: спочатку йде існуючий тип, потім нове ім'я:

typedef double Seconds;  // Seconds — це double
//      ^^^^^^  ^^^^^^^
//      існуючий   нове ім'я

Цей порядок не схожий на звичайне оголошення змінної (double x) і нерідко плутає новачків. Саме через цю незручність у C++11 з'явився новий синтаксис — розглянемо його у наступному підході.

Конвенція суфіксу _t

У системному та стандартному C-коді прийнято додавати суфікс _t до псевдонімів типів: size_t, ptrdiff_t, uint32_t. Ця конвенція сигналізує: «це псевдонім типу, а не змінна». У власному коді ця конвенція необов'язкова — деякі команди надають перевагу PascalCase (як вище: Seconds, Meters). Головне — бути послідовним у межах проєкту.

Технічно стандарт C зарезервував усі ідентифікатори, що закінчуються на _t, для майбутнього розширення стандартної бібліотеки. Щоб уникнути потенційних конфліктів у власному коді, надавайте перевагу PascalCase для користувацьких псевдонімів.

Підхід 2: чотири причини застосовувати псевдоніми

Причина 1 — Читабельність

Порівняємо дві сигнатури функції, яка повертає оцінку:

int gradeTest(int score, int maxScore, int passingScore);

Три параметри типу int. Яке семантичне значення кожного? Невідомо без документації. Що повертає функція — кількість балів? Категорію? Відсоток?

Причина 2 — Підтримка (maintainability)

Уявімо, що спочатку оцінки зберігалися як int, але пізніше виявилося, що потрібна точність до десятих (88.5 балів). Без псевдоніма довелося б замінити int на double у кожному місці використання — і не пропустити жодного. З псевдонімом — одна зміна:

// Було:
typedef int TestScore;

// Стало — один рядок змінився, весь інший код незмінний:
typedef double TestScore;

Увесь код, що використовує TestScore, автоматично починає працювати з double без жодних додаткових правок.

Причина 3 — Кросплатформність

Розмір базових типів у C++ залежить від платформи та компілятора. int на 32-бітній платформі — 4 байти, але специфікація цього не гарантує. Якщо ваш код передає числові дані по мережі або записує у бінарний файл, різниця у розмірі між платформами призведе до корупції даних.

Стандарт C++11 визначає у заголовку <cstdint> псевдоніми з гарантованим розміром:

ТипРозмірЗнакОпис
int8_t8 бітЗі знакомМалий цілий
uint8_t8 бітБез знакуБайт, символ
int16_t16 бітЗі знакомКороткий цілий
uint16_t16 бітБез знаку
int32_t32 бітЗі знакомЗвичайний цілий
uint32_t32 бітБез знакуНатуральний
int64_t64 бітЗі знакомВеликий цілий
uint64_t64 бітБез знаку
size_tзалежить від платформиБез знакуРозміри об'єктів, індекси
ptrdiff_tзалежить від платформиЗі знакомРізниця вказівників
NetworkPacket.cpp
#include <iostream>
#include <cstdint>

// Структура мережевого пакету — розміри полів фіксовані на будь-якій платформі
struct PacketHeader
{
    uint16_t sourcePort;      // 2 байти — завжди
    uint16_t destinationPort; // 2 байти — завжди
    uint32_t sequenceNumber;  // 4 байти — завжди
    uint8_t  flags;           // 1 байт — завжди
};

int main()
{
    std::cout << "sizeof(PacketHeader) = " << sizeof(PacketHeader) << " байт\n";
    return 0;
}
./NetworkPacket
$ ./NetworkPacket
sizeof(PacketHeader) = 10 байт
Execution finished with exit code 0.

Причина 4 — Спрощення складних типів

Повернемося до прикладу зі вступу. З псевдонімом він перетворюється на читабельний:

#include <vector>
#include <utility>
#include <string>

typedef std::vector<std::pair<std::string, int>> StudentScoreList;

StudentScoreList loadStudentScores(const std::string& filename);
void printStudentScores(StudentScoreList scores);
bool saveStudentScores(StudentScoreList scores, const std::string& path);

Тепер прототипи зрозумілі, а зміна внутрішнього представлення — в одному місці.


Підхід 3: typedef і вказівники на функції

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

// Параметр-вказівник на функцію: важко читати з першого погляду
void selectionSort(int* array, int size, bool (*compare)(int, int));

Що таке bool (*compare)(int, int)? Читач, незнайомий із синтаксисом вказівників на функції, бачить лише набір символів. Рішення — дати цьому типу змістовне ім'я.

Крок 1: наївний підхід — коментар

// compare — вказівник на функцію: bool f(int, int)
void selectionSort(int* array, int size, bool (*compare)(int, int));

Коментар допомагає, але він живе окремо від коду і може «протухнути» — якщо сигнатура зміниться, а коментар забудуть оновити.

Крок 2: typedef для типу вказівника на функцію

Comparator.cpp
#include <iostream>
#include <utility>

// Оголошуємо псевдонім: Comparator — це тип "вказівник на функцію bool(int,int)"
typedef bool (*Comparator)(int, int);

void selectionSort(int* array, int size, Comparator compare)
{
    for (int startIndex = 0; startIndex < size; ++startIndex)
    {
        int bestIndex = startIndex;

        for (int currentIndex = startIndex + 1; currentIndex < size; ++currentIndex)
        {
            if (compare(array[bestIndex], array[currentIndex]))
                bestIndex = currentIndex;
        }

        std::swap(array[startIndex], array[bestIndex]);
    }
}

bool ascending(int a, int b)  { return a > b; }
bool descending(int a, int b) { return a < b; }

void printArray(int* array, int size)
{
    for (int i = 0; i < size; ++i)
        std::cout << array[i] << ' ';
    std::cout << '\n';
}

int main()
{
    int numbers[] = { 3, 1, 7, 4, 2, 8, 5, 6 };

    selectionSort(numbers, 8, ascending);
    printArray(numbers, 8);

    selectionSort(numbers, 8, descending);
    printArray(numbers, 8);

    return 0;
}
./Comparator
$ ./Comparator
1 2 3 4 5 6 7 8
8 7 6 5 4 3 2 1
Execution finished with exit code 0.

Тепер сигнатура selectionSort(int* array, int size, Comparator compare) читається природно: третій аргумент — «компаратор». Ніяких (*compare) у прототипі.

Синтаксис typedef для вказівників на функції нетривіальний: ім'я псевдоніма пишеться всередині конструкції, а не після неї — typedef bool (*Comparator)(int, int). Саме цю незручність усуває using, розглянутий далі.

Підхід 4: using — сучасний синтаксис (C++11)

Стандарт C++11 ввів альтернативний синтаксис для псевдонімів типів через ключове слово using. Семантично він повністю еквівалентний typedef, але читається у природному для людини напрямку — зліва направо.

Порівняння синтаксису

// typedef — старий стиль: спочатку тип, потім ім'я
typedef double Seconds;
typedef std::vector<std::pair<std::string, int>> StudentScoreList;
typedef bool (*Comparator)(int, int);

// using — новий стиль C++11: ім'я = тип
using Seconds           = double;
using StudentScoreList  = std::vector<std::pair<std::string, int>>;
using Comparator        = bool (*)(int, int);

Форма using читається як рівність: «Seconds — це double». Порядок відповідає тому, як ми думаємо: спочатку нове ім'я, потім визначення. Для вказівників на функції — ім'я псевдоніма не «ховається» всередині конструкції.

У сучасному C++ рекомендується завжди використовувати using замість typedef. Стандарт не оголошує typedef застарілим, але using є стандартним підходом у новому коді відповідно до C++ Core Guidelines.

Повний приклад з using

TypeAliases.cpp
#include <iostream>
#include <cstdint>
#include <utility>

using Meters    = double;
using Seconds   = double;
using StudentId = uint32_t;
using Comparator = bool (*)(int, int);

double calculateSpeed(Meters distance, Seconds time)
{
    return distance / time;
}

void printId(StudentId id)
{
    std::cout << "Student #" << id << "\n";
}

bool ascending(int a, int b) { return a > b; }

int main()
{
    Meters dist   = 100.0;
    Seconds time  = 9.58;

    std::cout << "Швидкість: " << calculateSpeed(dist, time) << " м/с\n";

    StudentId id = 1001;
    printId(id);

    Comparator cmp = ascending;
    std::cout << "ascending(5, 3) = " << cmp(5, 3) << "\n";

    return 0;
}
./TypeAliases
$ ./TypeAliases
Швидкість: 10.4384 м/с
Student #1001
ascending(5, 3) = 1
Execution finished with exit code 0.

Підхід 5: поширені помилки та анти-патерни

Помилка 1 — псевдонім замість справжньої безпеки типів

Найважливіше, що треба розуміти: псевдонім не створює новий тип. Компілятор вважає псевдонім і оригінальний тип абсолютно взаємозамінними:

using Meters  = double;
using Seconds = double;

void move(Meters distance, Seconds time) { /* ... */ }

int main()
{
    Seconds t = 9.58;
    Meters  d = 100.0;

    move(t, d); // ✅ компілюється без помилки — але аргументи переплутані!
    return 0;
}

Якщо вам потрібна справжня безпека типів (щоб Meters і Seconds були несумісними), потрібні обгортки — структури або класи. Псевдоніми лише покращують читабельність, але не додають захисту від переплутування.

Не плутайте читабельність із безпекою. using Meters = double — це лише косметична зміна для читача. Щоб move(timeValue, distanceValue) став помилкою компіляції, потрібно окремо будувати типи-обгортки (strong typedef pattern).

Помилка 2 — надмірне використання

Псевдоніми шкодять читабельності, коли вживаються без реальної потреби:

// ❌ Надмірно: який сенс псевдоніма для простого int?
using Counter = int;
using Flag    = bool;
using Letter  = char;

Counter i = 0;  // це просто int-лічильник, читач тепер змушений пам'ятати псевдонім

Правило: вводьте псевдонім лише тоді, коли він додає семантичну інформацію або спрощує складний тип. using Meters = double — так, несе сенс. using Counter = int — зайве.

Помилка 3 — псевдонім у глобальному просторі імен у заголовковому файлі

// MyTypes.h — небезпечно!
using BigList = std::vector<std::pair<std::string, int>>;

Якщо цей заголовок підключений у багатьох файлах, BigList забруднює глобальний простір імен усього проєкту. Краще розміщувати псевдоніми всередині namespace або struct.


Практика

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

Рівень 2 — Логіка та системи

Рівень 3 — Архітектура


Резюме

Псевдоніми типів — це інструмент читабельності та підтримки коду, а не засіб захисту від помилок типізації.

Ключові висновки:

  • typedef існуючийТип новеІм'я — класичний синтаксис, успадкований від C; порядок слів незвичний.
  • using новеІм'я = існуючийТип — сучасний синтаксис C++11; читається природніше, є стандартом у новому коді.
  • Псевдонім не створює новий тип: компілятор бачить псевдонім і оригінал як взаємозамінні.
  • Чотири виправданих застосування: читабельність, підтримка (одна зміна), кросплатформність (<cstdint>), спрощення складних типів.
  • using для вказівників на функції читабельніший за typedef: ім'я не ховається всередині синтаксичної конструкції.
  • Використовуйте псевдоніми лише коли вони додають семантичну цінність — надмірне їх застосування погіршує читабельність.
У наступній статті «Структури (struct ми побачимо, як об'єднувати дані різних типів в одну сутність — і де псевдоніми типів стануть у пригоді для документування полів структури.
Copyright © 2026