%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Один обобщённый шаблон заменяет множество почти одинаковых реализаций под конкретные типы"
%%| fig-width: 6
%%| fig-height: 3
flowchart TB
Dup["Max(int,int)<br/>Max(float,float)<br/>Max(double,double)<br/>Max(Temperature,Temperature)"]
Tpl["template<typename T><br/>T Max(T a, T b)"]
Dup --> Tpl
W5. Шаблоны C++, метапрограммирование RANGE, умные указатели
1. Краткое содержание
1.1 Зачем нужны шаблоны? Проблема дублирования кода
Представьте, что нужна функция, возвращающая максимум из двух целых. Это просто:
int Max(int a, int b) {
return a > b ? a : b;
}Теперь та же логика нужна для float, double и пользовательского типа Temperature. Наивный путь — написать отдельную функцию под каждый тип:
float Max(float a, float b) { return a > b ? a : b; }
double Max(double a, double b) { return a > b ? a : b; }
// ... и так далее для каждого типаУ такого подхода есть серьёзные недостатки:
- Сложно сопровождать: любое исправление ошибки приходится переносить во все копии; тесты должны покрывать все варианты.
- Нереализуемо для библиотек: автор библиотеки заранее не знает все типы, которые понадобятся пользователю.
- Плохо масштабируется: в больших программах (тысячи типов) это становится неуправляемым.
Выход — genericity (обобщённость, параметрический полиморфизм): записать алгоритм один раз и позволить компилятору автоматически порождать версии под конкретные типы. В C++ это делает механизм templates (шаблонов). В других языках похожие идеи называются иначе:
- Ada, Eiffel: Generics
- Java, C#, Rust, Swift, Scala: Generics
- C++: Templates
1.2 Шаблоны функций (function templates)
1.2.1 Синтаксис
Function template (шаблон функции) — это «чертёж» для семейства функций. Вместо конкретного типа вроде int используется type parameter (параметр типа) — имя-заполнитель, обычно T:
template <typename T> // Template header: declares T as a type parameter
T Max(T a, T b) // Template body: uses T wherever the type appears
{
return a > b ? a : b;
}
// No semicolon after the closing bracetemplate— ключевое слово, с которого начинается объявление шаблона.typename T— объявляетTформальным параметром типа (можно писать иclass T— здесь это эквивалентно).- Тело шаблона такое же, как для конкретного типа, только вместо него везде стоит
T.
1.2.2 Инстанцирование шаблона (template instantiation): что происходит «под капотом»
Когда вы вызываете шаблон функции, компилятор выполняет template instantiation (инстанцирование шаблона): смотрит на типы фактических аргументов, порождает конкретную функцию (её называют function-by-template — функцией, полученной из шаблона), и компилирует её.
double x = 3.5, y = 2.1, res;
res = Max(x, y); // Compiler sees: both args are double
// → generates and compiles: double Max_double(double a, double b) { ... }
// → replaces the call with: res = Max_double(x, y);Весь процесс automatic (автоматический) — вы не пишете Max_double сами: это делает компилятор. Имя Max_double здесь условное; реальный компилятор использует name mangling (искажение имён).
Ключевые правила инстанцирования:
- Фактический тип выводится по типам аргументов, а не по типу возвращаемого значения.
- Каждая уникальная комбинация типов инстанцируется один раз, даже если вызывать функцию много раз с теми же типами.
- Каждая другая комбинация типов даёт отдельную function-by-template:
res = Max(x, y); // Generates Max_double
int k = Max(1, (int)res); // Generates Max_int (a different function!)1.2.3 Алгоритм инстанцирования (по шагам)
Когда компилятор встречает вызов f(actual-arguments):
- Если
f— обычная функция (regular function) → вызов компилируется напрямую. - Если
f— function template:- Определить тип \(T_i\) каждого фактического аргумента.
- Если function-by-template для этого набора \(\{T_i\}\) уже существует → перейти к шагу 3.
- Сгенерировать function-by-template, подставив \(T_i\) вместо формальных параметров типа.
- Скомпилировать сгенерированную функцию.
- Сгенерировать код вызова функции.
1.2.4 Раздувание кода (code bloat)
Инстанцирование шаблонов может привести к code bloat (раздуванию кода): если два отдельно компилируемых файла включают один и тот же заголовок с шаблоном и используют одни типы, линкер может получить две одинаковые копии одной и той же function-by-template в исполняемом файле:
T.h → File1.cpp (includes T.h) → File1.obj (contains Max_double)
→ File2.cpp (includes T.h) → File2.obj (contains Max_double)
→ App.exe (TWO copies of Max_double!)
Современные линкеры умеют сливать дубликаты, а стандарт C++ даёт средства (explicit instantiation, extern template) для контроля — но в крупных проектах это реальная забота.
1.2.5 Требования к фактическим типам
Шаблон не работает с любым типом — только с теми, для которых определены все операции, используемые в теле. Для Max в теле есть a > b, значит фактический тип должен иметь operator>.
class C {
int m;
public:
C() : m(0) { }
// No operator> defined!
};
C c1, c2;
Max(c1, c2); // Compile error: 'binary >': 'class C' doesn't define this operatorСообщение об ошибке обычно указывает на сгенерированную функцию вроде Max_C.
Решение: добавить в класс нужный оператор:
class C {
int m;
public:
C() : m(0) { }
bool operator>(const C& c) const { return m > c.m; }
};
// Now Max(c1, c2) works correctlyОбщий принцип: function template неявно требует, чтобы каждый фактический тип поддерживал все операции из тела шаблона. Нарушение — ошибка времени компиляции (compile-time error).
1.2.6 C++20 Concepts: формальные требования к типу
До C++20 эти требования оформлялись неформально (комментариями) и проявлялись только при инстанцировании — часто с запутанными диагностиками. Concepts (концепты) позволяют формально зафиксировать требования:
template<typename T>
concept GreaterThan =
requires(T x, T y) { { x > y } -> std::same_as<bool>; };
template<typename T> requires GreaterThan<T>
T Max(T a, T b) {
return a > b ? a : b;
}Теперь при вызове Max с типом без operator> компилятор сообщает о нарушенном concept, а не о «внутренней» ошибке глубоко в теле шаблона.
1.2.7 Явное инстанцирование шаблонов функций
Иногда компилятор не может вывести параметр шаблона из аргументов — например, у функции нет параметров:
template <typename T>
int spaceOf() {
int bytes = sizeof(T);
return bytes / 4 + (bytes % 4 > 0 ? 1 : 0);
}
// int w = spaceOf(); // ERROR: compiler cannot deduce TВыход — explicit instantiation (явное инстанцирование): указать тип в угловых скобках в месте вызова:
int wint = spaceOf<int>(); // Explicitly: T = int
int wdouble = spaceOf<double>(); // Explicitly: T = doubleКомпилятор инстанцирует шаблон с заданным типом и может применить оптимизации (например, sizeof(int) — compile-time constant, и всю функцию можно свернуть в константу):
spaceOf<int>() → generates spaceOf_int() → inlines to constant 1
int wint = spaceOf<int>(); → int wint = 1;
1.3 Шаблоны классов (class templates)
Class template (шаблон класса) — чертёж для семейства классов, так же как function template задаёт семейство функций.
- Класс — это тип.
- Шаблон класса — не тип; это семейство типов.
1.3.1 Мотивирующий пример: стек
Stack (стек), ещё говорят LIFO (Last In, First Out — «последним пришёл, первым ушёл») — структура данных с тремя базовыми операциями:
- push: положить элемент наверх.
- pop: снять и вернуть верхний элемент.
- isEmpty: проверить, пуст ли стек.
Реализация на C++ без шаблонов для целых выглядит так:
class Stack {
int top;
int S[100]; // Array of integers
public:
Stack() : top(-1) { }
void push(int V) { S[++top] = V; }
int pop() { return S[top--]; }
bool isEmpty() { return top < 0; }
};Чтобы получить стек double, пришлось бы копировать весь класс и заменить каждый int на double — та же проблема дублирования, что и у шаблонов функций. Решение через шаблон:
template <typename T> // T is the element type
class Stack {
int top;
T S[100]; // Array of T
public:
Stack() : top(-1) { }
void push(T V) { S[++top] = V; }
T pop() { return S[top--]; }
bool isEmpty() { return top < 0; }
};Теперь T может быть любым типом — целым, double, строкой или пользовательским.
1.3.2 Инстанцирование шаблона класса
В отличие от шаблонов функций (их инстанцируют неявно по типам аргументов вызова), class template нужно инстанцировать явно, синтаксисом <actual-type>:
Stack<int> sint; // Stack of integers
Stack<double> sdouble; // Stack of doubles
Stack<string> sstr; // Stack of strings
Stack<float> sf1, sf2; // Two stacks of floats
Stack<int>* arrayOfStacks[10]; // Array of 10 pointers to int-stacksЗапись Stack<int> — это type specifier (спецификатор типа): имя класса, который компилятор порождает из шаблона Stack, подставив int вместо T. Такой класс ведёт себя как обычный.
Использование class-by-template:
Stack<int> s;
s.push(1);
s.push(2);
int v = s.pop(); // v = 2Type aliases (псевдонимы типов) — два эквивалентных синтаксиса:
typedef Stack<double> SD; // C++98/03
using SD = Stack<double>; // C++11 (preferred)
SD sd1, sd2;1.3.3 Шаблон, класс и экземпляр — три уровня
Важно различать три сущности:
| Уровень | Сущность | Кто создаёт | Когда существует |
|---|---|---|---|
| Шаблон | Stack<T> (чертёж) |
программист | исходный код |
| Class-by-template | Stack<int>, Stack<double> |
компилятор (instantiation) | compile time |
| Объект (instance) | Stack<int> s; |
runtime (new или стек) |
runtime |
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "От определения шаблона к инстанцированному классу и объекту в runtime"
%%| fig-width: 6.3
%%| fig-height: 3
flowchart LR
Def["template<typename T><br/>class Stack"]
Spec1["Stack<int>"]
Spec2["Stack<double>"]
Obj["Stack<int> s"]
Def --> Spec1
Def --> Spec2
Spec1 --> Obj
1.3.4 Требования к фактическим типам в шаблонах классов
Внутри Stack метод push делает:
void push(T V) { S[++top] = V; }Используются две операции над T:
- Передача
Vпо значению (T V) — вызывается copy constructor (конструктор копирования). - Присваивание
= V— вызывается assignment operator (оператор присваивания).
Поэтому для Stack<MyClass> у MyClass должны быть публичный конструктор копирования и публичный оператор присваивания. Компилятор обычно генерирует их сам, но бывают исключения (дорогое копирование, = delete). Чтобы не вызывать копирование при входе в push, передавайте по ссылке:
void push(const T& V) { S[++top] = V; } // No copy constructor needed for the callАналогично, pop по значению тоже копирует. Часто улучшают возвратом по ссылке:
T& pop() { return S[top--]; }1.3.5 Нетиповые параметры шаблона (non-type template parameters)
Параметры шаблона — не обязаны быть типами: это могут быть constant values (константные значения) — целые, указатели и т.д. Тогда поведение или «размеры» класса задаются на compile time, а не только тип элемента.
Фиксированный массив T S[100] в Stack — ограничение дизайна: у каждого стека ровно 100 ячеек. Размер можно сделать параметром шаблона:
template <typename T, int N> // Two parameters: type T and integer N
class Stack {
int top;
T S[N]; // Size is now N, determined at compile time
public:
Stack() : top(-1) { }
void push(const T& V) { S[++top] = V; }
T pop() { top--; return S[top + 1]; }
bool isEmpty() { return top < 0; }
};Использование:
Stack<int, 10> s10; // Stack of 10 integers
Stack<double, 50> s50; // Stack of 50 doublesВажное ограничение: аргументы non-type шаблона должны быть compile-time constants. Переменную runtime в такой параметр подставить нельзя.
Допустимые non-type аргументы:
- константные целочисленные выражения (например
10,sizeof(int) * 4); - имена объектов с external linkage;
- адреса объектов/функций с external linkage.
1.3.6 Шаблоны классов с несколькими параметрами типа
У шаблона может быть несколько параметров типа. Для Dictionary (отображение ключ → значение) естественно два типа:
#include <map>
#include <string>
template<typename K, typename V>
class Dictionary {
std::map<K, V> data;
public:
void insert(const K& key, const V& value) {
data[key] = value;
}
V get(const K& key) const {
auto it = data.find(key);
if (it != data.end()) return it->second;
throw std::runtime_error("Key not found");
}
bool contains(const K& key) const {
return data.find(key) != data.end();
}
void remove(const K& key) {
data.erase(key);
}
};
// Usage:
Dictionary<int, std::string> myDict;
myDict.insert(1, "One");
myDict.insert(2, "Two");
std::cout << myDict.get(1) << std::endl; // "One"1.4 Метапрограммирование на примере типов RANGE
Metaprogramming (метапрограммирование) — использование системы типов и шаблонов, чтобы проверять ограничения на этапе компиляции, а не в runtime. Пример RANGE это хорошо иллюстрирует.
1.4.1 Проблема «голого» целого
Пусть currentDay должен хранить день месяца (1–31). Если взять int:
int currentDay, currentMonth;
currentDay = 70; // абсурд, но компилятор это пропустит
currentDay = currentMonth + 1; // смешать день и месяц? предупреждения не будетВ Pascal и Ada это решают range types (типами-диапазонами):
type DayOfMonth = 1..31; // Pascaltype DayOfMonth is Integer range 1..31; -- AdaВ C++ встроенных range types нет, но на классах и шаблонах такой тип можно построить.
1.4.2 Первая попытка: наивный класс RANGE
Идея: класс хранит значение вместе с допустимыми границами и проверяет значение при каждом изменении.
class RANGE {
int leftBorder;
int rightBorder;
int value;
public:
// Constructor: set borders and initial value
RANGE(int v, int l, int r) {
leftBorder = l;
rightBorder = r;
value = v;
check(); // Verify immediately
}
// Deleted default constructor: forbid uninitialized RANGE objects
RANGE() = delete;
// Copy constructor
RANGE(const RANGE& r) {
leftBorder = r.leftBorder;
rightBorder = r.rightBorder;
value = r.value;
}
// Assignment from another RANGE
RANGE& operator=(RANGE& r) { value = r.value; return *this; }
// Assignment from int
RANGE& operator=(int v) { value = v; check(); return *this; }
// Increment
RANGE& operator++() { value++; check(); return *this; }
// Conversion to int (so RANGE can be used where int is expected)
operator int() { return value; }
private:
void check() {
if (value < leftBorder || value > rightBorder)
throw std::out_of_range("RANGE value out of bounds");
}
};Почему operator= возвращает *this? Чтобы работали цепочки присваиваний: в a = b = c присваивание должно возвращать ссылку на левый операнд.
Использование:
RANGE range(0, -10, 10); // value=0, allowed: [-10, 10]
range = 5; // OK
range = 15; // throws exception
++range; // increments, checks
int i = range; // uses operator int()1.4.3 Недостатки наивного подхода
Код работает, но есть два фундаментальных слабых места:
Проблема 1 — границы в значении, а не в типе:
RANGE a(0, -5, 5);
RANGE b(3, 1, 10);
a = b; // Compiles! But semantically wrong — different ranges!a и b имеют один и тот же тип (RANGE), хотя описывают разные области значений. Хотелось бы, чтобы RANGE(-5,5) и RANGE(1,10) были разными типами, и присваивание одного другому давало ошибку компиляции (compile-time error).
Проблема 2 — лишняя память:
В каждом объекте RANGE три целых: value, leftBorder, rightBorder. Границы после конструктора не меняются — логически это часть типа, а не значения. Нет смысла хранить их в каждом экземпляре.
1.4.4 Шаблонное решение для RANGE
Границы делаем template parameters (параметрами шаблона) — compile-time constants, а не полями runtime. Тогда каждая пара (leftBorder, rightBorder) порождает отдельный тип:
template <int leftBorder, int rightBorder>
class RANGE {
int value; // Only member: the stored value
RANGE() = delete; // No default constructor
public:
RANGE(int v) { value = v; check(); }
RANGE(const RANGE& r) { value = r.value; }
RANGE& operator=(RANGE& r) { value = r.value; return *this; }
RANGE& operator=(int v) { value = v; check(); return *this; }
RANGE& operator++() { value++; check(); return *this; }
operator long() { return (long)value; }
private:
void check() {
if (value < leftBorder || value > rightBorder)
throw std::out_of_range("RANGE value out of bounds");
}
};Теперь:
RANGE<-5, 5> a(0); // Type: RANGE<-5,5>, stores only one int
RANGE<1, 10> b(3); // Type: RANGE<1,10>, completely different type
a = b; // COMPILE ERROR: different types — assignment operator is type-safe!RANGE<-5,5> и RANGE<1,10> — разные классы, сгенерированные из одного шаблона. В объекте хранится одно целое; границы «запечены» в имени типа на этапе компиляции — нулевая стоимость в runtime (zero runtime overhead).
Type aliases упрощают запись:
typedef RANGE<-5, 5> myTinyInt; // C++98
using myTinyInt = RANGE<-5, 5>; // C++11 (preferred)
myTinyInt i = 2; // Clean, type-safe syntaxСемейства RANGE<-5,5>, RANGE<1,10>, RANGE<100,1000> и т.д. — взаимно несовместимые типы из одного шаблона, подобно тому как несовместимы int и double.
1.5 Проблемы «сырых» указателей C/C++ (raw pointers)
Прежде чем исправлять указатели, важно понять, в чём беда. Скотт Мейерс выделил шесть категорий проблем raw pointers (плюс другие).
1.5.1 Неоднозначность: один объект или массив (проблемы 1 и 4)
Указатель T* ptr может указывать и на один объект, и на первый элемент массива — по одному типу указателя это не различить:
int x;
int A1[10];
int* A2 = &x;
int* A = cond ? A1 : A2;
int res = A[5]; // Is this valid? Depends on cond — undefined behavior!Отсюда два разных оператора освобождения — delete ptr (один объект) и delete[] ptr (массив); перепутать их — undefined behavior (неопределённое поведение).
1.5.2 Неясность владения (проблема 2)
По объявлению T* ptr в сигнатуре функции не видно, владеет ли функция объектом (должна ли его уничтожать):
void fun(T* ptr) {
// Do some work with *ptr
// Should we destroy it? We don't know!
return;
}Такая неоднозначность ведёт к memory leaks (никто не вызвал delete) или к double-deletion (несколько путей пытаются удалить одно и то же).
1.5.3 Неясность способа уничтожения (проблема 3)
Даже если «надо удалить», неочевидно как:
void fun(T* ptr) {
// Work...
free(ptr); // Correct? Or should it be:
// myDealloc(ptr); // a custom deallocator?
// delete ptr; // Or this?
}Разные схемы выделения требуют разных функций освобождения.
1.5.4 Двойное уничтожение (проблема 5)
Когда указатель разделяют несколько путей кода, легко уничтожить объект дважды:
void lib_fun(T* ptr) {
// Does lib_fun delete the object? Hard to know without reading all source code.
}
void user_fun() {
T* ptr = new T();
lib_fun(ptr);
delete ptr; // Is this a double-delete if lib_fun already deleted it?
}Double-deletion — undefined behavior: может повредить heap и открыть уязвимости.
1.5.5 Висячие указатели (dangling pointers), проблема 6
Встроенного способа узнать, жив ли ещё объект по указателю, нет:
T* ptr = new T();
if (condition) delete ptr;
// ...
// Long code later...
// Is ptr still valid? Cannot know without tracking control flow manually.Dangling pointer (висячий указатель) — ссылается на память, уже освобождённую. Разыменование — undefined behavior.
Классический висячий указатель со стеком:
int* p;
void f() {
int A[10];
p = A + 2; // p points into f's stack frame
} // A goes out of scope — memory is freed
int main() {
f();
*p = 777; // p is now dangling — undefined behavior!
}После возврата из f кадр стека (вместе с A) недействителен, а p всё ещё хранит адрес в этой памяти.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Висячий указатель: указатель переживает уничтоженный стековый объект"
%%| fig-width: 6.2
%%| fig-height: 3.2
flowchart TB
subgraph Frame["Кадр стека функции f"]
A["A : int = 42"]
P["p = &A"]
end
After["возврат из f<br/>кадр снят"]
Dangling["p хранит старый адрес<br/>dangling pointer"]
Frame --> After --> Dangling
style Frame fill:#f9fbfd,stroke:#355c7d,color:#1f2d3d
style A fill:#d6eef5,stroke:#355c7d,color:#1f2d3d
style P fill:#e8f4f8,stroke:#355c7d,color:#1f2d3d
style After fill:#eef3f7,stroke:#355c7d,color:#1f2d3d
style Dangling fill:#f9d9e2,stroke:#355c7d,color:#1f2d3d
1.5.6 Утечки памяти (проблема 7)
Если последний (или единственный) указатель на объект в heap выходит из области видимости без delete, объект остаётся в памяти, но недостижим — это memory leak (утечка памяти):
void f() {
int* p = new int(42); // Dynamic object allocated on heap
// ... (no delete)
} // p goes out of scope; the int(42) still lives on the heap but cannot be reached!В долгоживущих процессах накопленные утечки могут исчерпать память.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Утечка: объект в куче остаётся выделенным, когда владеющий указатель исчез"
%%| fig-width: 6
%%| fig-height: 3
flowchart LR
P["p : int*"]
H["объект в куче<br/>new int(42)"]
Gone["p вышел из области видимости"]
Leak["недостижимый объект<br/>memory leak"]
P --> H
H --> Gone --> Leak
1.6 Умные указатели (smart pointers)
Smart pointers (умные указатели) — это class templates, оборачивающие raw pointers и добавляющие автоматическое управление ресурсом: «умное» поведение вроде автоматического delete при сохранении эффективности, близкой к сырому указателю. Нужен #include <memory>.
Ключевой приём — RAII (Resource Acquisition Is Initialization): ресурс захватывается в конструкторе и освобождается в деструкторе. Деструктор вызывается при выходе объекта из области видимости, значит очистка предсказуема.
1.6.1 Общая идея: простейший умный указатель
Минимальный вариант оборачивает raw pointer и вызывает delete при уничтожении обёртки:
template <typename T>
class smart_pointer {
T* obj; // The underlying raw pointer
public:
smart_pointer(T* o) : obj(o) { } // Takes ownership
~smart_pointer() { delete obj; } // Guaranteed cleanup
T* operator->() { return obj; } // Arrow dereference
T& operator*() { return *obj; } // Star dereference
};{
smart_pointer<MyClass> sp(new MyClass());
sp->someMethod(); // Works like a raw pointer
} // Destructor called — MyClass object is deleted automaticallyВызывающему коду delete не нужен. В стандартной библиотеке C++ — три «промышленных» шаблона умных указателей.
1.6.2 unique_ptr — исключительное владение
unique_ptr реализует exclusive ownership (исключительное владение): в каждый момент ровно один unique_ptr владеет объектом. Копирование запрещено на уровне типа:
#include <memory>
std::unique_ptr<int> x(new int(42));
std::unique_ptr<int> y;
y = x; // COMPILE ERROR: copy is forbidden
std::unique_ptr<int> z(x); // COMPILE ERROR: copy constructor is deletedПередача владения — через std::move: владение переносится, источник обнуляется (nullifies):
y = std::move(x); // y now owns the object; x becomes nullptrФабрика C++14 — make_unique: предпочтительнее голого new, потому что exception-safe (безопаснее при исключениях):
auto x = std::make_unique<int>(42); // No explicit newОсновные методы:
x.get()— сырой указатель без отказа от владения;x.reset()— уничтожить объект и обнулить указатель;x.release()— вернуть сырой указатель и отказаться от владения (дальше ответственность на вызывающем).
unique_ptr снимает:
- memory leaks (автоудаление);
- double deletion (единственный владелец);
- неясность владения (намерение видно из типа).
1.6.5 weak_ptr — разрыв циклов
weak_ptr — non-owning (не владеющий) наблюдатель за объектом под shared_ptr. Он не увеличивает reference count, поэтому не удерживает объект от удаления. Зато можно проверить, жив ли объект, и временно получить shared_ptr для безопасного доступа.
auto shared = std::make_shared<MyObj>(); // ref count = 1
std::weak_ptr<MyObj> weak(shared); // ref count still = 1 (weak does not count)
shared = nullptr; // ref count drops to 0 — object deleted
// weak now points to an expired (destroyed) object
if (weak.expired()) {
// Object is gone
}
auto temp = weak.lock(); // Attempts to get a shared_ptr
if (temp) {
// Object still exists — use temp safely
}Разрыв цикла через weak_ptr: одну дугу цикла делают weak_ptr:
class Bar;
class Foo {
public:
std::shared_ptr<Bar> bar; // Foo owns Bar (strong reference)
};
class Bar {
public:
std::weak_ptr<Foo> foo; // Bar observes Foo but does NOT own it (weak reference)
};
void fun() {
auto foo = std::make_shared<Foo>(); // foo ref count = 1
foo->bar = std::make_shared<Bar>(); // bar ref count = 1
foo->bar->foo = foo; // foo ref count stays 1 (weak_ptr!)
}
// fun exits: foo ref count = 1 → 0 → Foo deleted → Bar's shared_ptr lost → bar ref count = 1 → 0 → Bar deleted
// No leak!Сводка по типам умных указателей:
| Тип | Владение | Копия | ref count | Когда использовать |
|---|---|---|---|---|
unique_ptr |
Exclusive | Нет (только move) | Нет | Один владелец, без разделения |
shared_ptr |
Shared | Да | Да (ARC) | Несколько владельцев |
weak_ptr |
Нет (наблюдатель) | Да | Нет | Разрыв циклов, «слабые» ссылки |
2. Определения
- Template (шаблон): средство C++, позволяющее писать обобщённый код с параметрами типа или значений compile time, чтобы компилятор порождал конкретные функции или классы под заданный тип.
- Function template (шаблон функции): шаблон как чертёж семейства функций; компилятор каждый раз порождает конкретную function-by-template при использовании с конкретным типом.
- Class template (шаблон класса): шаблон как чертёж семейства классов; перед использованием нужно явное инстанцирование вида
TemplateName<ActualType>. - Template parameter (параметр шаблона): заполнитель в определении шаблона — либо type parameter (
typename T/class T), либо non-type parameter (константа compile time, напримерint N). - Template instantiation (инстанцирование шаблона): процесс, в котором компилятор подставляет фактические типы (или значения) вместо формальных параметров и получает конкретную функцию или класс.
- Function-by-template: конкретная функция после инстанцирования function template (условно
Max_doubleизMax<T>). - Class-by-template: конкретный класс после инстанцирования class template (например
Stack<int>изStack<T>). - Implicit instantiation (неявное инстанцирование): инстанцирование, которое компилятор запускает сам при вызове function template с типизированными аргументами.
- Explicit instantiation (явное инстанцирование): программист явно задаёт тип в угловых скобках (например
spaceOf<int>()), когда тип из аргументов вывести нельзя. - Non-type template parameter: параметр шаблона — константное значение compile time (часто целое), а не тип; например
template <typename T, int N>. - Code bloat (раздувание кода): когда инстанцирование даёт несколько копий одной и той же function-by-template в разных объектных файлах и раздувает исполняемый файл.
- Type requirements (требования к типу): набор операций, который фактический тип должен поддерживать в шаблоне; иначе — compile-time errors.
- Concept (C++20) (концепт): формальное, проверяемое компилятором описание требований к параметрам шаблона; яснее диагностика при нарушении.
- Genericity (обобщённость): возможность писать код для любого типа, удовлетворяющего требованиям, а не только для фиксированных типов.
- RANGE type: пользовательский тип, ограничивающий целые значения интервалом на compile time; обычно class template с non-type границами.
- Metaprogramming (метапрограммирование): техника, где программа на compile time манипулирует типами/значениями через шаблоны, а не логикой runtime.
- Raw pointer (сырой указатель): обычный указатель C/C++ (
T*) без встроенной семантики владения и автоочистки. - Dangling pointer (висячий указатель): указатель на память уже освобождённую или вышедшую из области видимости; разыменование — undefined behavior.
- Memory leak (утечка памяти): объект в heap не освобождён, потому что последний указатель исчез без
delete; память занята, но недоступна. - RAII (Resource Acquisition Is Initialization): идиома C++ — ресурс (память, дескриптор файла, mutex и т.д.) захватывается в конструкторе и освобождается в деструкторе при выходе объекта из области видимости.
- Smart pointer (умный указатель): class template, оборачивающий raw pointer и добавляющий управление ресурсом (и при необходимости учёт владения) через RAII.
unique_ptr: exclusive ownership — в каждый момент владеет объектом не более одногоunique_ptr; при уничтожении указателя объект удаляется. Копирование запрещено; владение переносится черезstd::move.shared_ptr: shared ownership через ARC; объект удаляется, когда уничтожается последнийshared_ptrна него.weak_ptr: non-owning наблюдатель за объектом подshared_ptrбез роста reference count; разрывает circular references. Доступ — черезshared_ptrизlock().- Automatic Reference Counting (ARC): стратегия с целочисленным reference count; объект уничтожается при нуле счётчика владельцев.
- Reference count: счётчик, который ведёт
shared_ptrдля числа владеющихshared_ptrданным объектом. - Circular reference (циклическая ссылка): два и более объекта держат
shared_ptrдруг на друга; счётчики не падают до нуля — memory leak. std::move: приведение к rvalue reference; перенос владения уunique_ptr(и других move-only типов) без копирования.make_unique<T>(...)(C++14): фабрика, создающая объект и оборачивающая вunique_ptrза один exception-safe шаг.make_shared<T>(...): фабрика, создающая объект и control block со счётчиками вместе, возвращаетshared_ptr.weak_ptr::expired():true, если наблюдаемый объект уже уничтожен (счётчик владельцев обнулился).weak_ptr::lock(): пытается получитьshared_ptrна объект; если объекта нет — пустойshared_ptr.- Control block (блок управления): внутренняя структура рядом с управляемым объектом в
shared_ptr; хранит reference count и счётчик weak ссылок.
3. Примеры
3.1. Обобщённый стек и специализация StringStack (Лаба 5, Задание 1)
Реализуйте class template GenericStack<T> с операциями push(), pop() и peek(). Стек должен динамически менять размер. Затем создайте подкласс StringStack, который:
- при
push()отклоняет пустые строки; - добавляет метод
concatTopTwo(): снимает две верхние строки, конкатенирует и кладёт результат обратно.
Предусмотрите обработку ошибок на граничных случаях.
Нажмите, чтобы увидеть решение
Ключевая идея: templates и inheritance работают вместе: StringStack наследует GenericStack<string>, получает всю обобщённую функциональность и настраивает/расширяет её для строк.
#include <iostream>
#include <vector>
#include <stdexcept>
#include <string>
using namespace std;
// -------- GenericStack<T> --------
template <typename T>
class GenericStack {
protected:
vector<T> data; // vector handles dynamic resizing automatically
public:
// Constructor: optional initial capacity (vector still grows as needed)
explicit GenericStack(int initialCapacity = 0) {
data.reserve(initialCapacity);
}
virtual ~GenericStack() = default;
// Insert element on top
virtual void push(const T& element) {
data.push_back(element);
}
// Remove and return top element
virtual T pop() {
if (isEmpty()) throw underflow_error("pop() on empty stack");
T top = data.back();
data.pop_back();
return top;
}
// Return top element without removing it
virtual T peek() const {
if (isEmpty()) throw underflow_error("peek() on empty stack");
return data.back();
}
bool isEmpty() const { return data.empty(); }
int size() const { return (int)data.size(); }
};
// -------- StringStack --------
class StringStack : public GenericStack<string> {
public:
explicit StringStack(int capacity = 0) : GenericStack<string>(capacity) { }
// Override push: reject empty strings
void push(const string& s) override {
if (s.empty())
throw invalid_argument("StringStack::push: empty string not allowed");
GenericStack<string>::push(s); // Delegate to base
}
// New method: pop top two strings, concatenate, push result
void concatTopTwo() {
if (size() < 2)
throw underflow_error("concatTopTwo: need at least 2 elements");
string second = pop(); // top
string first = pop(); // second from top
push(first + second); // push concatenation
}
};
int main() {
cout << "=== GenericStack<int> ===" << endl;
GenericStack<int> intStack(5);
intStack.push(10);
intStack.push(20);
intStack.push(30);
cout << "Peek: " << intStack.peek() << endl; // 30
cout << "Pop: " << intStack.pop() << endl; // 30
cout << "Pop: " << intStack.pop() << endl; // 20
cout << "\n=== StringStack ===" << endl;
StringStack ss;
ss.push("Hello, ");
ss.push("World!");
cout << "Top before concat: " << ss.peek() << endl; // "World!"
ss.concatTopTwo();
cout << "After concatTopTwo: " << ss.peek() << endl; // "Hello, World!"
// Try pushing empty string
try {
ss.push("");
} catch (const invalid_argument& e) {
cout << "Error: " << e.what() << endl;
}
// Try concatTopTwo on single element
try {
ss.concatTopTwo();
} catch (const underflow_error& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}Вывод:
=== GenericStack<int> ===
Peek: 30
Pop: 30
Pop: 20
=== StringStack ===
Top before concat: World!
After concatTopTwo: Hello, World!
Error: StringStack::push: empty string not allowed
Error: concatTopTwo: need at least 2 elements
- База шаблона:
GenericStack<T>работает с любым типом;vector<T>сам растёт при необходимости. - Наследование:
StringStack : public GenericStack<string>наследует все обобщённые операции. - Переопределение
push: сначала проверка, затем делегирование в базу черезGenericStack<string>::push(s). concatTopTwo: снятие в порядке LIFO — первым снимается верх (второе слово), вторым — под ним (первое слово); конкатенация должна восстановить правильный порядок фразы.- Виртуальный деструктор базы:
virtual ~GenericStack() = defaultгарантирует корректное уничтожение через указатель на базу.
Ответ: полная реализация приведена выше. Inheritance вместе с templates даёт аккуратную специализацию обобщённого контейнера.
3.2. Умные указатели на примере класса Box (Лаба 5, Задание 2)
Создайте класс Box с целым полем, конструктором и деструктором (с выводом в консоль). Затем реализуйте и продемонстрируйте:
(a) create_unique(int val) — создаёт Box через unique_ptr, показывает перенос владения и возвращает значение внутри Box.
(b) create_shared_boxes() — создаёт два shared_ptr<Box> и показывает, как меняется reference count.
(c) пример с weak_ptr<Box> — проверка «живости» и получение shared_ptr через lock() для доступа. Объясните, как weak_ptr устраняет circular references.
Нажмите, чтобы увидеть решение
Ключевая идея: у каждого вида smart pointer своя модель владения: unique_ptr — один владелец; shared_ptr — shared ownership и ARC; weak_ptr — non-owning наблюдатель.
#include <iostream>
#include <memory>
using namespace std;
// ---- Box class ----
class Box {
public:
int value;
Box(int v) : value(v) {
cout << "Box(" << value << ") created\n";
}
~Box() {
cout << "Box(" << value << ") destroyed\n";
}
};
// ---- (a) unique_ptr: exclusive ownership ----
int create_unique(int val) {
auto box = make_unique<Box>(val); // Box created, ref count = 1 (conceptually)
cout << "box value: " << box->value << endl;
// Transfer ownership: box2 takes over, box becomes null
auto box2 = move(box);
cout << "After move: box is " << (box ? "valid" : "null") << endl;
cout << "box2 value: " << box2->value << endl;
int result = box2->value;
return result;
// box2 goes out of scope → Box destroyed automatically
}
// ---- (b) shared_ptr: cooperative ownership ----
void create_shared_boxes() {
auto boxA = make_shared<Box>(10); // ref count = 1
cout << "boxA ref count: " << boxA.use_count() << endl; // 1
auto boxB = make_shared<Box>(20); // ref count = 1
cout << "boxB ref count: " << boxB.use_count() << endl; // 1
{
auto boxA2 = boxA; // ref count = 2 (shared ownership)
cout << "After boxA2 = boxA, ref count: " << boxA.use_count() << endl; // 2
auto boxA3 = boxA; // ref count = 3
cout << "After boxA3 = boxA, ref count: " << boxA.use_count() << endl; // 3
}
// boxA2 and boxA3 go out of scope → ref count = 1, object NOT deleted
cout << "After scope: boxA ref count: " << boxA.use_count() << endl; // 1
}
// boxA goes out of scope → ref count = 0 → Box(10) destroyed
// ---- (c) weak_ptr: non-owning reference ----
void demonstrate_weak() {
auto shared = make_shared<Box>(42); // ref count = 1
weak_ptr<Box> weak(shared); // ref count still = 1
cout << "shared ref count: " << shared.use_count() << endl; // 1
cout << "weak expired? " << weak.expired() << endl; // 0 (false)
// Safe access via lock()
if (auto temp = weak.lock()) {
cout << "Accessed via weak: " << temp->value << endl; // 42
}
shared.reset(); // Explicitly destroy the shared_ptr → ref count = 0 → Box destroyed
cout << "After reset, weak expired? " << weak.expired() << endl; // 1 (true)
if (!weak.lock()) {
cout << "Object is gone — weak_ptr is safe to use (returns null)\n";
}
}
int main() {
cout << "=== (a) unique_ptr ===" << endl;
int v = create_unique(100);
cout << "Returned value: " << v << "\n" << endl;
cout << "=== (b) shared_ptr ===" << endl;
create_shared_boxes();
cout << endl;
cout << "=== (c) weak_ptr ===" << endl;
demonstrate_weak();
return 0;
}Вывод:
=== (a) unique_ptr ===
Box(100) created
box value: 100
After move: box is null
box2 value: 100
Box(100) destroyed
Returned value: 100
=== (b) shared_ptr ===
Box(10) created
boxA ref count: 1
Box(20) created
boxB ref count: 1
After boxA2 = boxA, ref count: 2
After boxA3 = boxA, ref count: 3
After scope: boxA ref count: 1
Box(10) destroyed
Box(20) destroyed
=== (c) weak_ptr ===
shared ref count: 1
weak expired? 0
Accessed via weak: 42
Box(42) destroyed
After reset, weak expired? 1
Object is gone — weak_ptr is safe to use (returns null)
unique_ptr(a):move(box)переносит владение и обнуляетbox. Объект уничтожается, когдаbox2выходит из области видимостиcreate_unique.shared_ptr(b): каждая копияshared_ptr(присваивание или копирующий конструктор) увеличиваетuse_count(). Когда все копии уничтожены,use_count()становится 0 и объект удаляется.weak_ptr(c):weak_ptr<Box> weak(shared)— наблюдатель без роста reference count;shared.reset()обнуляет счётчик владельцев →Boxудаляется;- после уничтожения
weak.expired()даётtrue; weak.lock()безопасно возвращает пустойshared_ptr— без undefined behavior.
- Циклы: если бы в
Boxбыли взаимныеshared_ptr<Box>, объекты не освободились бы; одну дугу заменяют наweak_ptr<Box>.
Ответ: полный код выше демонстрирует все три smart pointer и семантику времени жизни.
3.3. Шаблон функции Max: максимум из двух значений (Лекция 5, Пример 1)
Напишите function template Max, возвращающий максимум из двух значений любого типа с operator>. Покажите вызовы для int, double и пользовательского класса.
Нажмите, чтобы увидеть решение
Ключевая идея: function template пишется один раз с параметром типа T; компилятор порождает конкретные версии (Max_int, Max_double и т.д.) по типам аргументов в каждой точке вызова.
#include <iostream>
using namespace std;
// Function template: works for any type T that has operator>
template <typename T>
T Max(T a, T b) {
return a > b ? a : b;
}
// A user-defined class that satisfies the template requirement (has operator>)
class Temperature {
double celsius;
public:
Temperature(double c) : celsius(c) { }
bool operator>(const Temperature& t) const { return celsius > t.celsius; }
double value() const { return celsius; }
};
int main() {
// Implicit instantiation for int (compiler generates Max_int)
cout << Max(3, 7) << endl; // 7
// Implicit instantiation for double (compiler generates Max_double)
cout << Max(3.14, 2.71) << endl; // 3.14
// Implicit instantiation for Temperature (compiler generates Max_Temperature)
Temperature t1(36.6), t2(38.2);
cout << Max(t1, t2).value() << endl; // 38.2
return 0;
}- Объявление шаблона:
template <typename T>вводит параметр типаT; телоreturn a > b ? a : b;используетTвезде, где нужен тип. - Implicit instantiation: для
Max(3, 7)компилятор выводитT = intи внутренне порождает функцию видаint Max_int(int a, int b). - Требования к типу: у
Temperatureдолжен бытьoperator>— иначе ошибка внутри сгенерированнойMax_Temperature. - Разные инстансы:
Max_int,Max_doubleиMax_Temperature— три разные функции в скомпилированном коде.
Ответ: из одного определения шаблона получаются три функции. Вывод: 7, 3.14, 38.2.
3.4. alignArray: от конкретной функции к шаблону (Лекция 5, Пример 2)
По следующей функции только для int постройте function template для массивов произвольного «числового» типа элементов. Объявите класс, удовлетворяющий type requirements шаблона, и продемонстрируйте шаблон на массиве этого класса.
void alignArray(int* array, int size, int barrier) {
for (int i = 0; i < size; i++) {
if (array[i] < barrier) array[i] += 2;
else if (array[i] > barrier) array[i] -= 2;
}
}Нажмите, чтобы увидеть решение
Ключевая идея: перечислите операции, которые функция применяет к элементам; это и есть требования к параметру типа T.
- Операции над элементами:
<,>,+=,-=— типTдолжен поддерживать все четыре. - Шаблон:
#include <iostream>
using namespace std;
template <typename T>
void alignArray(T* array, int size, T barrier) {
for (int i = 0; i < size; i++) {
if (array[i] < barrier) array[i] += 2;
else if (array[i] > barrier) array[i] -= 2;
}
}- Класс, удовлетворяющий требованиям:
class Score {
int val;
public:
Score(int v = 0) : val(v) { }
bool operator<(const Score& s) const { return val < s.val; }
bool operator>(const Score& s) const { return val > s.val; }
Score& operator+=(int n) { val += n; return *this; }
Score& operator-=(int n) { val -= n; return *this; }
int value() const { return val; }
};- Демонстрация на классе:
int main() {
// Demo with int
int intArr[] = {1, 5, 10, 3, 7};
alignArray(intArr, 5, 5);
for (int x : intArr) cout << x << " "; // 3 5 8 5 5
cout << endl;
// Demo with Score
Score scores[] = {Score(1), Score(8), Score(5), Score(3)};
alignArray(scores, 4, Score(5));
for (auto& s : scores) cout << s.value() << " "; // 3 6 5 5
cout << endl;
return 0;
}Ответ: шаблон задаёт и тип элементов, и тип порога одним параметром T. Любой тип с <, >, +=, -= подходит.
3.5. Шаблон стека с non-type параметром размера (Лекция 5, Пример 3)
Реализуйте обобщённый class template Stack с параметрами: тип элементов T и максимальный размер N. Покажите создание стеков разных типов и размеров.
Нажмите, чтобы увидеть решение
Ключевая идея: non-type template parameters задают «габариты» класса на compile time. Stack<int,10> и Stack<int,50> — разные типы.
#include <iostream>
#include <stdexcept>
using namespace std;
template <typename T, int N>
class Stack {
int top;
T S[N]; // Array size N is a compile-time constant
public:
Stack() : top(-1) { }
void push(const T& V) {
if (top >= N - 1) throw overflow_error("Stack is full");
S[++top] = V;
}
T pop() {
if (top < 0) throw underflow_error("Stack is empty");
return S[top--];
}
T peek() const {
if (top < 0) throw underflow_error("Stack is empty");
return S[top];
}
bool isEmpty() const { return top < 0; }
bool isFull() const { return top >= N - 1; }
};
int main() {
Stack<int, 10> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);
cout << intStack.pop() << endl; // 3 (LIFO order)
cout << intStack.pop() << endl; // 2
Stack<string, 5> strStack;
strStack.push("hello");
strStack.push("world");
cout << strStack.pop() << endl; // "world"
// Stack<int, 10> and Stack<int, 50> are DIFFERENT types:
// Stack<int, 50> bigStack = intStack; // COMPILE ERROR
return 0;
}- Объявление шаблона:
template <typename T, int N>— два параметра: тип элементов и вместимость. - Массив:
T S[N]—Nвычисляется на compile time и годится как размер массива. - Семантика LIFO:
pushувеличиваетtopперед записью;popчитает наtop, затем уменьшает. - Контроль границ: при переполнении/пустом стеке — стандартные исключения.
Ответ: Stack<int,10> и Stack<string,5> — независимые типы из одного шаблона. Вывод: 3, 2, world.
3.6. spaceOf: явное инстанцирование шаблона функции (Лекция 5, Пример 4)
Напишите function template spaceOf<T>(), который считает, сколько 32-битных слов (по 4 байта) нужно, чтобы разместить значение типа T. Покажите explicit instantiation для нескольких типов.
Нажмите, чтобы увидеть решение
Ключевая идея: если у шаблона функции нет аргументов для вывода T, тип нужно указать явно — синтаксисом <T> в месте вызова.
#include <iostream>
using namespace std;
template <typename T>
int spaceOf() {
int bytes = sizeof(T);
// Number of 32-bit words: ceiling division by 4
return bytes / 4 + (bytes % 4 > 0 ? 1 : 0);
}
class MyData {
double x, y, z; // 3 doubles = 24 bytes
int flag; // 4 bytes
// Total: 28 bytes = 7 × 4-byte words
};
int main() {
// Cannot call spaceOf() without explicit type — no arguments to deduce from
cout << spaceOf<int>() << endl; // 4 bytes → 1 word
cout << spaceOf<double>() << endl; // 8 bytes → 2 words
cout << spaceOf<char>() << endl; // 1 byte → 1 word (ceiling)
cout << spaceOf<MyData>() << endl; // 28+ bytes → 7+ words (depends on padding)
// The compiler optimizes: sizeof(int)=4 is a compile-time constant,
// so spaceOf<int>() is likely inlined to the constant 1.
return 0;
}- Нечем вывести тип: у
spaceOf()нет параметров, компилятор не выведетT. - Явный синтаксис:
spaceOf<int>()— угловые скобки задаютT = int. - Оптимизация на этапе компиляции:
sizeof(T)— compile-time constant, функция может полностью свернуться в константу. - Формула потолка:
bytes/4 + (bytes%4 > 0 ? 1 : 0)даёт \(\lceil \text{bytes}/4 \rceil\).
Ответ: spaceOf<int>() = 1, spaceOf<double>() = 2, spaceOf<char>() = 1.
3.7. Шаблон RANGE: полная реализация (Туториал 5, Пример 1)
Напишите полную реализацию шаблона RANGE, включая:
- конструктор(ы) и деструктор;
- арифметические и отношения (
+=,-=,+,-,==,!=,<,>); - операторы инкремента и декремента;
- приведение
RANGE → long; - проверку границ и исключения при нарушении.
Приведите два реалистичных примера использования RANGE.
Нажмите, чтобы увидеть решение
Ключевая идея: non-type template parameters переносят границы диапазона в тип, а не в значение. Поэтому RANGE<1,12> и RANGE<1,31> — несовместимые типы: компилятор не даст их перепутать.
#include <iostream>
#include <stdexcept>
using namespace std;
template <int L, int R>
class RANGE {
int value;
void check() const {
if (value < L || value > R)
throw out_of_range("RANGE value " + to_string(value)
+ " out of [" + to_string(L) + "," + to_string(R) + "]");
}
public:
// Constructor from int
explicit RANGE(int v) : value(v) { check(); }
// Copy constructor
RANGE(const RANGE& r) : value(r.value) { }
// Destructor (trivial)
~RANGE() = default;
// --- Assignment ---
RANGE& operator=(const RANGE& r) { value = r.value; return *this; }
RANGE& operator=(int v) { value = v; check(); return *this; }
// --- Compound arithmetic ---
RANGE& operator+=(int n) { value += n; check(); return *this; }
RANGE& operator-=(int n) { value -= n; check(); return *this; }
// --- Binary arithmetic (return plain int for flexibility) ---
int operator+(int n) const { return value + n; }
int operator-(int n) const { return value - n; }
int operator+(const RANGE& r) const { return value + r.value; }
int operator-(const RANGE& r) const { return value - r.value; }
// --- Increment / Decrement ---
RANGE& operator++() { value++; check(); return *this; } // pre-increment
RANGE operator++(int) { RANGE tmp(*this); ++(*this); return tmp; } // post-increment
RANGE& operator--() { value--; check(); return *this; } // pre-decrement
RANGE operator--(int) { RANGE tmp(*this); --(*this); return tmp; } // post-decrement
// --- Relational ---
bool operator==(const RANGE& r) const { return value == r.value; }
bool operator!=(const RANGE& r) const { return value != r.value; }
bool operator< (const RANGE& r) const { return value < r.value; }
bool operator> (const RANGE& r) const { return value > r.value; }
// --- Conversion to long ---
operator long() const { return (long)value; }
};
// ---- Practical Examples ----
using DayOfMonth = RANGE<1, 31>;
using MonthOfYear = RANGE<1, 12>;
int main() {
// Example 1: Calendar date arithmetic
DayOfMonth day(15);
MonthOfYear month(6);
cout << "Day: " << (long)day << endl; // 15
cout << "Month: " << (long)month << endl; // 6
++day;
cout << "Next day: " << (long)day << endl; // 16
try {
day = 32; // out of range!
} catch (const out_of_range& e) {
cout << "Error: " << e.what() << endl;
}
// day = month; // COMPILE ERROR: incompatible types — safety guaranteed!
// Example 2: Traffic light phase (0=Red, 1=Yellow, 2=Green)
using Phase = RANGE<0, 2>;
Phase light(0);
for (int i = 0; i < 4; i++) {
cout << "Light phase: " << (long)light << endl;
try { ++light; } catch (...) { light = Phase(0); } // wrap around on overflow
}
return 0;
}- Параметры шаблона как идентичность типа:
DayOfMonth(псевдонимRANGE<1,31>) иMonthOfYear(RANGE<1,12>) — разные типы; смешать их — ошибка компиляции. check()на каждой мутации: конструктор,operator=,operator+=,operator++и т.д.- Приведение к
long: удобно печатать и подставлять значение в выражения без лишних приведений. - Постфиксный
++: нужна копия «старого» значения до инкремента.
Ответ: полный код выше. Границы фиксируются и на compile time (через тип), и на runtime (через исключения).
3.8. Шаблон ARRAY с границами индекса как у RANGE (Туториал 5, Пример 2)
Спроектируйте и реализуйте шаблон ARRAY, где допустимый диапазон индексов задаётся параметрами шаблона (по духу как у RANGE). Массив должен поддерживать произвольные границы индекса (не обязательно с нуля). Реализуйте operator[] с проверкой границ. Покажите практический пример.
Нажмите, чтобы увидеть решение
Ключевая идея: как RANGE вносит границы значения в тип, так ARRAY может внести границы индекса. ARRAY<int,1,12> — 12 целых с индексами 1…12, т.е. «массив с базой 1».
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// ARRAY<T, Low, High>: an array of type T with indices in [Low, High]
template <typename T, int Low, int High>
class ARRAY {
static_assert(Low <= High, "ARRAY: Low must be <= High");
static const int SIZE = High - Low + 1;
T data[SIZE];
void checkIndex(int idx) const {
if (idx < Low || idx > High)
throw out_of_range("ARRAY index " + to_string(idx)
+ " out of [" + to_string(Low) + "," + to_string(High) + "]");
}
public:
ARRAY() = default;
// Non-const indexing (allows modification)
T& operator[](int idx) {
checkIndex(idx);
return data[idx - Low]; // Shift: external index → internal 0-based index
}
// Const indexing (for read-only access)
const T& operator[](int idx) const {
checkIndex(idx);
return data[idx - Low];
}
int low() const { return Low; }
int high() const { return High; }
int size() const { return SIZE; }
};
int main() {
// 1-based array of month names (indices 1..12)
ARRAY<string, 1, 12> monthNames;
monthNames[1] = "January";
monthNames[2] = "February";
monthNames[3] = "March";
// ... (fill remaining)
monthNames[12] = "December";
cout << monthNames[1] << endl; // "January"
cout << monthNames[12] << endl; // "December"
try {
monthNames[0]; // Index 0 is out of range [1, 12]
} catch (const out_of_range& e) {
cout << "Error: " << e.what() << endl;
}
// 2D array using ARRAY of ARRAYs:
ARRAY<ARRAY<int, 1, 3>, 1, 3> matrix;
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
matrix[i][j] = i * 10 + j;
cout << matrix[2][3] << endl; // 23
return 0;
}- Сдвиг индекса: внутри хранение 0-основано (
data[0..SIZE-1]); снаружи индексы[Low..High]; формулаdata[idx - Low]. static_assert: проверка аргументов шаблона на compile time (напримерARRAY<int, 10, 5>не скомпилируется с ясным сообщением).- Два
operator[]: для изменяемого доступа (T&) и дляconstмассива (const T&). - 2D: вложенный
ARRAYвARRAYдаёт матрицу с проверкой обоих индексов.
Ответ: ключевой приём — сдвиг data[idx - Low]. Любой диапазон индексов задаётся типом; выход за границы ловится в runtime.
3.9. Обобщённый умный указатель: реализация RAII (Туториал 5, Пример 3)
Реализуйте простой class template smart_pointer<T>, который:
- принимает владение raw pointer, переданным в конструктор;
- автоматически удаляет объект при выходе умного указателя из области видимости;
- поддерживает
->и*в стиле обычного указателя; - добавьте операторы, максимально приближающие поведение к raw pointer;
- напишите тест, показывающий преимущество перед «сырыми» указателями.
Нажмите, чтобы увидеть решение
Ключевая идея: RAII гарантирует вызов деструктора при выходе из области видимости — в том числе при раскрутке стека по исключению. На этом строятся все smart pointers.
#include <iostream>
#include <stdexcept>
using namespace std;
template <typename T>
class smart_pointer {
T* obj;
public:
// Constructor: acquire ownership
explicit smart_pointer(T* o = nullptr) : obj(o) { }
// Destructor: release ownership (RAII core)
~smart_pointer() {
delete obj;
cout << "[smart_pointer: object deleted]" << endl;
}
// Prevent copying (like unique_ptr)
smart_pointer(const smart_pointer&) = delete;
smart_pointer& operator=(const smart_pointer&) = delete;
// Pointer-like operators
T* operator->() { return obj; }
T& operator*() { return *obj; }
// Conversion to raw pointer (read-only)
T* get() const { return obj; }
// Boolean check: is the pointer non-null?
explicit operator bool() const { return obj != nullptr; }
// Comparison with nullptr
bool operator==(nullptr_t) const { return obj == nullptr; }
bool operator!=(nullptr_t) const { return obj != nullptr; }
};
// A sample class to manage
class Resource {
int id;
public:
Resource(int i) : id(i) { cout << "Resource " << id << " created\n"; }
~Resource() { cout << "Resource " << id << " destroyed\n"; }
void use() { cout << "Using Resource " << id << "\n"; }
};
void demonstrateAdvantage() {
// With raw pointers: must remember to delete!
// Resource* raw = new Resource(99);
// raw->use();
// if (someCondition) return; // MEMORY LEAK if we forget delete here
// delete raw;
// With smart_pointer: automatic cleanup, even on early return
smart_pointer<Resource> sp(new Resource(1));
sp->use(); // Works like a raw pointer
(*sp).use(); // Also works
if (sp) {
cout << "Pointer is valid\n";
}
cout << "Leaving scope...\n";
} // smart_pointer destructor called automatically here — no delete needed!
int main() {
demonstrateAdvantage();
cout << "After scope exit\n";
return 0;
}Вывод:
Resource 1 created
Using Resource 1
Using Resource 1
Pointer is valid
Leaving scope...
[smart_pointer: object deleted]
Resource 1 destroyed
After scope exit
- RAII:
~smart_pointer()вызываетdelete obj— при выходе из области или при исключении. - Запрет копирования: иначе два
smart_pointerмогли бы владеть одним объектом (double-delete). operator->: возвращает сырой указатель для синтаксисаsp->use().operator bool(): проверки видаif (sp).- Плюс над raw: даже при раннем выходе или исключении деструктор сработает — нет memory leak.
Ответ: критичен деструктор по RAII; последовательность вывода выше показывает автоматическую очистку.