Українська
Шаблони класів
Шаблон класу та параметр-значення
FixedStack<int, 2> і FixedStack<int, 3> – різні типи. Місткість N входить до типу, а не є полем, яке можна змінити після створення. Це дозволяє компілятору визначити розмір масиву; за потреби змінної місткості доречніший контейнер із динамічною пам’яттю. Параметр std::size_t N приймає константний аргумент, доступний під час компіляції. Звичайне число, щойно прочитане з клавіатури, не є таким аргументом.
Інваріант стека: 0 <= used <= N; дійсні логічні елементи займають перші used комірок. Усі елементи std::array фізично існують увесь час. Тому це не універсальна реалізація контейнера для довільних типів: T повинен підтримувати ініціалізацію за замовчуванням і потрібні операції копіювання. Після pop об’єкт у комірці не знищується, а лише перестає належати логічному стеку. Це важливо для типів, що володіють ресурсом.
Стек фіксованої місткості
Умова. Зберегти два числа, отримати останнє; створити стек із масиву та перевірити нульову місткість.
cpp
#include <array>
#include <cstddef>
#include <print>
#include <stdexcept>
template<class T, std::size_t N>
class FixedStack {
std::array<T, N> data{};
std::size_t used = 0;
public:
FixedStack() = default;
explicit FixedStack(const std::array<T, N>& source)
: data(source), used(N) {}
void push(const T& value) {
if (used == N) throw std::length_error("full");
data[used] = value;
++used;
}
T pop() {
if (used == 0) throw std::out_of_range("empty");
T result = data[used - 1];
--used;
return result;
}
};
int main()
{
FixedStack<int, 2> small;
small.push(3);
small.push(8);
std::println("{}", small.pop());
FixedStack inferred(std::array{10, 20, 30});
std::println("{}", inferred.pop());
FixedStack<int, 0> zero;
try { zero.push(1); }
catch (const std::length_error& e) {
std::println("error: {}", e.what());
}
}Результат виконання:
text
8
30
error: fullВизначення push змінює лічильник лише після присвоювання. Якщо присвоювання кине виняток, лічильник залишиться старим. pop спочатку копіює результат, а потім зменшує лічильник. Однак копіювання або переміщення поверненого об’єкта також може мати власні винятки. Повної сильної гарантії для довільного T цей навчальний клас не обіцяє.
Конструктор, що приймає std::array<T, N>, дає компілятору інформацію для CTAD: у FixedStack inferred(std::array{10,20,30}) виводяться і T, і N. Порожнє FixedStack inferred; такої інформації не має. CTAD працює під час створення об’єкта, а не дозволяє всюди пропускати параметри назви типу, наприклад у довільній сигнатурі функції.
Для класів з іншими конструкторами можна записати deduction guide. Він не є функцією і не має тіла; він описує, які параметри класу слід вивести зі списку аргументів. Спочатку перевіряють, чи достатньо неявних правил, і лише потім додають власне правило виведення.
Значення за замовчуванням і залежні імена
Запис template<class T = int, std::size_t N = 8> задає типові аргументи. Об’єкт FixedStack<> усе ще використовує шаблон, хоча конкретні значення взято з оголошення. Значення за замовчуванням не слід приховувати, якщо вони впливають на місткість або одиниці вимірювання: це частина інтерфейсу. Параметр auto дозволяє вивести тип параметра-значення, наприклад template<auto Limit>, але різні типи констант можуть створити різні спеціалізації навіть за однакового числового значення.
Ім’я всередині шаблону може залежати від параметра: typename T::value_type повідомляє компілятору, що вкладене залежне ім’я позначає тип. Ключове слово typename у такій позиції не створює нового параметра. Визначаючи метод класу поза тілом, повторюють список параметрів і повну назву FixedStack<T, N>::push. Пропущений список не перетворює метод на нетипізовану функцію; це інший, некоректний запис.
Для читабельності довгі типи називають псевдонімом using. Псевдонім не створює нового типу, тому using Metres = double не забороняє додати секунди. Сильна типізація одиниць у лабораторному прикладі використовує окремі типи тегів, а не лише зручні назви.
Спеціалізація та перевантаження
Первинний шаблон задає загальну поведінку. Повна спеціалізація класу замінює її для точно визначених аргументів. Часткова спеціалізація задає сімейство, наприклад Holder<T*> для типів вказівників. Частково спеціалізувати шаблон функції не можна: для функцій зазвичай використовують перевантаження, обмеження або допоміжний клас.
Спеціалізація не успадковує автоматично методи первинного шаблону. Якщо інтерфейс має містити text, потрібно надати його і в кожній спеціалізації. В іншому разі клієнтський узагальнений код отримає різні інтерфейси, хоча назва сімейства класів залишиться однаковою. Спеціалізацію оголошують до першого використання, яке потребує інстанціювання відповідного варіанта.
Спеціалізація форматування
Умова. Для звичайного числа використати стандартний формат, для bool слова yes/no, для рядка квадратні дужки.
cpp
#include <format>
#include <print>
#include <string>
template<class T>
struct Serializer {
static std::string text(const T& value) {
return std::format("{}", value);
}
};
template<>
struct Serializer<bool> {
static std::string text(bool value) {
return value ? "yes" : "no";
}
};
template<>
struct Serializer<std::string> {
static std::string text(const std::string& value) {
return std::format("[{}]", value);
}
};
int main()
{
std::println("{}", Serializer<int>::text(42));
std::println("{}", Serializer<bool>::text(true));
std::println("{}",
Serializer<std::string>::text("sample"));
}Результат виконання:
text
42
yes
[sample]Це навчальне текстове подання, а не формат JSON чи безпечний протокол збереження. Символи дужок у початковому рядку не екрануються, і зворотний розбір тут не визначено. Назва Serializer описує роль прикладу, але реальна серіалізація потребує версії, правил екранування та обробки помилок.
Первинний шаблон працездатний лише для типів із підтримкою форматування. Сам факт оголошення Serializer<Custom> не обов’язково інстанціює тіло всіх методів. Помилка може з’явитися лише при виклику text. Це одна з причин перевіряти ті операції шаблону, які справді використовує клієнт.
Видимість визначення й помилки компонування
Звичайній функції для виклику достатньо оголошення; її визначення компонується з іншого об’єктного файла. Для неявного інстанціювання шаблону компілятору зазвичай потрібне видиме визначення. Тому визначення шаблонів розміщують у заголовку або включеному ним .tpp, а не ховають у довільному .cpp. Розширення .tpp не має магічної поведінки: важливо, щоб його текст реально був включений.
Якщо у math.h є лише template<class T> T twice(T);, а визначення лежить у math.cpp, компіляція клієнта може пройти, але компонувальник не знайде спеціалізацію twice<int>. Додавання ще одного #include до клієнта допомагає лише тоді, коли робить видимим саме визначення. Не слід включати один і той самий .cpp та одночасно компілювати його як окрему одиницю: це може створити дубльовані визначення.
Інший, свідомий дизайн – явне інстанціювання обмеженого набору типів у реалізації. Рядок template int twice<int>(int); змушує створити цей варіант у відповідній одиниці трансляції. Оголошення extern template у клієнті може пригнічувати повторне неявне інстанціювання. Такий підхід корисний для закритого переліку типів, але довільний новий тип уже не підтримується без зміни реалізації бібліотеки.
Шаблони в заголовках також підпорядковуються правилу одного визначення. Однакові токени й узгоджене розв’язання імен є суттєвими вимогами. Макрос, який змінює тіло шаблону в різних .cpp, може порушити ODR, навіть якщо компонувальник не видає очевидної помилки.