Псевдоніми типів (typedef і using)
Псевдоніми типів (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;
Тепер функція швидкості виглядає так:
#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;
}
Сигнатура 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). Головне — бути послідовним у межах проєкту.
_t, для майбутнього розширення стандартної бібліотеки. Щоб уникнути потенційних конфліктів у власному коді, надавайте перевагу PascalCase для користувацьких псевдонімів.Підхід 2: чотири причини застосовувати псевдоніми
Причина 1 — Читабельність
Порівняємо дві сигнатури функції, яка повертає оцінку:
int gradeTest(int score, int maxScore, int passingScore);
Три параметри типу int. Яке семантичне значення кожного? Невідомо без документації. Що повертає функція — кількість балів? Категорію? Відсоток?
typedef int TestScore;
typedef int PassingThreshold;
TestScore gradeTest(TestScore score, TestScore maxScore, PassingThreshold passing);
Тепер повертає TestScore, приймає TestScore і PassingThreshold. Семантика закодована в іменах типів.
Причина 2 — Підтримка (maintainability)
Уявімо, що спочатку оцінки зберігалися як int, але пізніше виявилося, що потрібна точність до десятих (88.5 балів). Без псевдоніма довелося б замінити int на double у кожному місці використання — і не пропустити жодного. З псевдонімом — одна зміна:
// Було:
typedef int TestScore;
// Стало — один рядок змінився, весь інший код незмінний:
typedef double TestScore;
Увесь код, що використовує TestScore, автоматично починає працювати з double без жодних додаткових правок.
Причина 3 — Кросплатформність
Розмір базових типів у C++ залежить від платформи та компілятора. int на 32-бітній платформі — 4 байти, але специфікація цього не гарантує. Якщо ваш код передає числові дані по мережі або записує у бінарний файл, різниця у розмірі між платформами призведе до корупції даних.
Стандарт C++11 визначає у заголовку <cstdint> псевдоніми з гарантованим розміром:
| Тип | Розмір | Знак | Опис |
|---|---|---|---|
int8_t | 8 біт | Зі знаком | Малий цілий |
uint8_t | 8 біт | Без знаку | Байт, символ |
int16_t | 16 біт | Зі знаком | Короткий цілий |
uint16_t | 16 біт | Без знаку | — |
int32_t | 32 біт | Зі знаком | Звичайний цілий |
uint32_t | 32 біт | Без знаку | Натуральний |
int64_t | 64 біт | Зі знаком | Великий цілий |
uint64_t | 64 біт | Без знаку | — |
size_t | залежить від платформи | Без знаку | Розміри об'єктів, індекси |
ptrdiff_t | залежить від платформи | Зі знаком | Різниця вказівників |
#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;
}
Причина 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 для типу вказівника на функцію
#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;
}
Тепер сигнатура 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». Порядок відповідає тому, як ми думаємо: спочатку нове ім'я, потім визначення. Для вказівників на функції — ім'я псевдоніма не «ховається» всередині конструкції.
using замість typedef. Стандарт не оголошує typedef застарілим, але using є стандартним підходом у новому коді відповідно до C++ Core Guidelines.Повний приклад з using
#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;
}
Підхід 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 — Базовий
using псевдоніми: Meters, Kilograms, Seconds (усі — double). Напишіть функцію double kineticEnergy(Kilograms mass, Meters perSecond), яка обчислює кінетичну енергію за формулою ½mv². У main() виведіть результат для mass = 70.0 кг, speed = 10.0 м/с.using StudentId = uint32_t (підключіть <cstdint>). Оголосіть масив з п'яти StudentId зі значеннями 1001..1005. Напишіть функцію bool isValidId(StudentId id), яка повертає true, якщо id >= 1000 && id <= 9999. Виведіть результат перевірки для кожного елемента масиву.Рівень 2 — Логіка та системи
using Predicate = bool (*)(int). Напишіть функцію int countIf(int* array, int size, Predicate pred), яка рахує кількість елементів, для яких предикат повертає true. Реалізуйте предикати isEven, isPositive, isGreaterThanTen. Виведіть результати для масиву {-5, 12, 3, 8, -1, 20, 7, 4, 15, -2}.Дано такий старий код:
typedef unsigned int uint;
typedef char* String;
typedef void (*Handler)(int);
Перепишіть усі три псевдоніми через using. Переконайтесь, що код компілюється і поведінка не змінилась. Напишіть коротку програму, що демонструє роботу кожного псевдоніма.
Рівень 3 — Архітектура
Реалізуйте заголовковий файл Platform.h з псевдонімами для числових типів, що автоматично перемикаються між 32-бітною і 64-бітною платформою:
#ifdef PLATFORM_64BIT
using NativeInt = int64_t;
using NativeUInt = uint64_t;
#else
using NativeInt = int32_t;
using NativeUInt = uint32_t;
#endif
using Byte = uint8_t;
using Address = uintptr_t;
У main.cpp підключіть цей заголовок і виведіть sizeof для кожного псевдоніма. Скомпілюйте двічі — з -DPLATFORM_64BIT і без — та порівняйте результати.
Резюме
Псевдоніми типів — це інструмент читабельності та підтримки коду, а не засіб захисту від помилок типізації.
Ключові висновки:
typedef існуючийТип новеІм'я— класичний синтаксис, успадкований від C; порядок слів незвичний.using новеІм'я = існуючийТип— сучасний синтаксис C++11; читається природніше, є стандартом у новому коді.- Псевдонім не створює новий тип: компілятор бачить псевдонім і оригінал як взаємозамінні.
- Чотири виправданих застосування: читабельність, підтримка (одна зміна), кросплатформність (
<cstdint>), спрощення складних типів. usingдля вказівників на функції читабельніший заtypedef: ім'я не ховається всередині синтаксичної конструкції.- Використовуйте псевдоніми лише коли вони додають семантичну цінність — надмірне їх застосування погіршує читабельність.
struct)» ми побачимо, як об'єднувати дані різних типів в одну сутність — і де псевдоніми типів стануть у пригоді для документування полів структури.Класи-перерахування (enum class)
Дізнайтеся, чому незахищений enum є небезпечним, як enum class вирішує проблеми простору імен і неявних конвертацій, та коли використовувати static_cast і базовий тип перерахування.
Системи числення та двійкова арифметика
Що таке системи числення, як працюють двійкова, вісімкова та шістнадцяткова системи, переведення між ними, та арифметичні операції у двійковій системі — база для розуміння бітових операцій у C++.