1Архитектура от А до Я
Тут шесть слоёв, сверху вниз. Каждый делает только своё и в чужое не лезет — я специально так раскидал, чтобы самому потом не путаться, где что.
┌─────────────────────────────────────────────────┐
│ Пользователь: m.insert({1, "hello"}) │ ← публичный API
└─────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ cpp_lab::MapTree<K, T, Tree, Compare> │
│ Фасад: at, operator[], try_emplace, │
│ insert_or_assign + проброс <using> │
└─────────────────────────────────────────────────┘
▼ (private inheritance)
┌─────────────────────────────────────────────────┐
│ cpp_lab::RBTree<K, T, C> ИЛИ AVLTree<K, T, C> │
│ Конкретные алгоритмы балансировки: │
│ emplace, link_and_fix, unlink_and_fix, │
│ rotate_left, rotate_right │
└─────────────────────────────────────────────────┘
▼ (public CRTP)
┌─────────────────────────────────────────────────┐
│ tree_detail::OrderedTreeBase<Derived,...> │
│ Общий API: begin/end, find, insert, erase, │
│ extract, merge, swap, copy/move/= default │
└─────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ tree_detail::TreeIterator<V, IsConst> │
│ ++, --, *, ->, паспорт итератора │
└─────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ tree_detail::NodeBase │
│ parent / left / right / is_header │
│ ▲ ▲ │
│ │ │ │
│ Node<Value> (value) │
│ ▲ ▲ │
│ │ │ │
│ RBNode (red) AVLNode (height) │
└─────────────────────────────────────────────────┘
OrderedTreeBase через CRTP дёргает методы наследника вообще без virtual. По сути это статический полиморфизм: общий код я держу в базе, а конкретную балансировку — в RBTree/AVLTree. В рантайме это не стоит ровным счётом ничего.
1.1 Полный путь m.insert({1, "hello"})
Разберу весь путь по шагам — от самого вызова до того, что в итоге вернётся пользователю.
m.insert({1, "hello"}) — создаётся временный std::pair<const int, std::string> (это rvalue) и передаётся в insert.using base::insertinsert унаследован от OrderedTreeBase через директиву using. Просто открывает доступ к base-методу.&&. Вызывает self().emplace(std::move(v)).this к RBTree* и вызывает emplace уже у наследника. Никаких virtual — всё на этапе компиляции.new tree_node(std::forward<Args>(args)...) — создаёт RBNode<pair> в куче, конструктор пары вызывается прямо в узле (perfect forwarding).compare_(key, node.key). Если ключ уже есть — узел удаляется, возвращается (it, false).p.left/right = z. Новый узел красят красным. Цикл восстанавливает правила RB: пока есть «два красных подряд» — перекраски и вращения.paint(root, false) — корень принудительно чёрный (правило 2 RB-дерева).std::pair<iterator, bool>: куда вставили и был ли вставлен новый элемент.1.2 Полный путь m[5] = "hello"
operator[](const Key&)try_emplace(key) и возвращает ссылку на second. Если ключа не было — создаст пару с default-конструированным значением.base::find(key) — если ключ есть, значение НЕ создаётся (экономия для тяжёлых T). Если нет — base::emplace(piecewise_construct, ...).pair имеет несколько перегрузок.= "hello" — присваивание по ссылке.1.3 Полный путь m.erase(5)
find(key), если не нашли — возвращаем 0. Если нашли — self().erase_one(it).unlink_and_fix, делает delete, уменьшает size.2Теория · то, что реально гоняют на защите
2.1 Шаблоны (templates)
Что это. Шаблон — это образец, по которому компилятор автоматически генерирует конкретный код под нужные типы. Один код — работает с int, string, твоим классом.
Чем шаблон отличается от макроса
- Макрос — тупая текстовая подстановка препроцессором, без проверки типов. Можно подсунуть что угодно — потом ошибки в странных местах.
- Шаблон — компилятор полностью разбирает описание шаблона: проверяет типы, перегрузки, корректность операций. Ошибка вылезет понятная (хоть и многословная).
Шаблонные параметры — что бывает
- Тип:
template<typename T>— самое частое. Подставляется тип. - Константа времени компиляции (non-type):
template<int N>. Так делают, например, факториал — см. бонусные задачи. - Шаблон шаблона (template template):
template<template<typename> class C>. У тебя в MapTree так передаётся Tree. - Variadic:
template<typename... Args>— пакет произвольного числа типов.
template <typename T1, typename T2, typename T3>
struct Triple {
T1 first;
T2 second;
T3 third;
};
Triple<int, int, int> point = {-1, 3, 2};
Triple<std::string, std::string, int> words = {"hi", "world", 42};
Компилятор генерирует ДВЕ никак не связанные структуры — это разные типы.
Специализация шаблона
- Полная — указываются ВСЕ параметры:
template<> void max<int>(...). Работает для функций и классов. - Частичная — указываются НЕ ВСЕ параметры. Работает только для классов, для функций — ошибка
function template partial specialization is not allowed.
Шаблонные классы и методы
Методы шаблонного класса тоже шаблонные. Можно описывать вне класса:
template <typename T1, typename T2, typename T3>
struct Triple {
T1 first; T2 second; T3 third;
T1 sum();
};
template <typename T1, typename T2, typename T3>
T1 Triple<T1, T2, T3>::sum() {
return first + second + third;
}
Дружественные функции
friend внутри шаблонного класса даёт указанной функции доступ к private/protected. У тебя в коде friend Base; в RBTree — позволяет базе вызывать твои приватные методы (reinsert, erase_one, detach).
Затрудняют ли шаблоны разработку?
- Ошибки многословные и диагностируются не там, где сделаны, а в шаблоне.
- Сложно писать разные реализации для разных категорий типов (приходится использовать SFINAE, концепты).
Как работают шаблоны при компиляции и линковке
Шаблон — это рецепт, а не код. Реальный код появляется на этапе компиляции, когда шаблон инстанцируется (подставляются конкретные типы).
- Компиляция .cpp: при каждом
vector<int>,vector<string>компилятор генерирует отдельный код этого класса. - Несколько .cpp видят один шаблон → дубликаты: если в
a.cppиb.cppестьvector<int>, оба сгенерируют этот класс. На этапе линковки линкер должен слить их в один (через специальный механизм —weak symbolsилиcomdat sections). - Поэтому шаблоны живут в .h-файлах: иначе один .cpp не увидит определение шаблона. Если хочешь вынести в .cpp — нужна явная инстанциация:
template<> int max<int>(int, int);
RBTree<int, string> и RBTree<double, MyClass> компилятор генерирует отдельно при первой встрече такой подстановки. Если один и тот же экземпляр используется в разных .cpp — линкер сливает их в один символ. Поэтому шаблоны принято писать целиком в .h.
2.2 Категории значений · lvalue / rvalue / xvalue / prvalue / glvalue
В современном C++ каждое выражение имеет: тип + категорию значений.
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue xvalue prvalue
'имею 'вот- (общее с
адрес' вот glvalue,
умру' но движимое)
lvalue (locator value, «локативное значение»)
Имеет имя, имеет адрес в памяти, продолжает жить.
int var; // var — lvalue
var = 4; // слева может стоять только lvalue
std::string name = "Alice"; // name — lvalue
// Функция, возвращающая lvalue-ссылку:
int& foo() { return globalvar; }
foo() = 10; // OK: foo() — lvalue, ему можно присваивать
prvalue (pure rvalue, «чистое rvalue»)
Временное, без идентичности. Литералы, результаты операций, временные объекты.
42, true, nullptr // все — prvalue
str.substr(1, 2) // результат функции по значению — prvalue
int x + 1 // результат арифметики — prvalue
// нельзя:
4 = var; // ERROR: prvalue не может быть слева от =
&42; // ERROR: нет адреса у prvalue
xvalue (eXpiring value, «истекающее»)
Временный объект, который вот-вот умрёт, но ИМЯ всё ещё имеет. Получается из std::move(x) и при возврате rvalue-ссылки из функции.
std::string s = "hello";
std::move(s); // xvalue: имя есть (s), но компилятор понимает
// "хозяин обещал больше им не пользоваться, можно красть"
Двойной амперсант && — rvalue-ссылка
Привязывается только к rvalue. Это «приёмник временного, у которого можно забрать ресурсы».
| Тип параметра | Что принимает |
|---|---|
T& | только lvalue |
const T& | что угодно (но без модификации) |
T&& | только rvalue |
T&& с T-шаблоном | что угодно (forwarding reference) |
Где в твоей лабе видно
// tree_base.h:447-448
std::pair<iterator, bool> insert(const value_type& v); // для lvalue → копия
std::pair<iterator, bool> insert(value_type&& v); // для rvalue → move
// std::move — преобразует свой аргумент в xvalue:
m.insert(std::move(pair)); // идёт во вторую перегрузку
foo(T&& x) сам x является lvalue (у него есть имя!). Чтобы передать дальше как rvalue — нужен std::move(x) или std::forward<T>(x).
=: имена переменных, элементы массива, разыменованный указатель. rvalue — временные значения без долговременной идентичности: литералы, результаты арифметики, возвраты функций по значению. T&& — rvalue-ссылка, принимающая только rvalue. Нужна, чтобы отличить временный объект, у которого можно «украсть» содержимое, от постоянного — это основа move-семантики и оптимизации копирований.
2.3 Правило 5 · правило 3 · правило 0
Что это и что объединяет
Это специальные функции-члены класса. Если ты определяешь одну из них вручную — обычно нужно определить ВСЕ, иначе компилятор сгенерирует остальные неправильно (для класса, владеющего ресурсами).
| Правило | Что входит |
|---|---|
| Правило 3 (C++98) | деструктор + копи-конструктор + копи-оператор |
| Правило 5 (C++11) | + move-конструктор + move-оператор присваивания |
| Правило 0 (modern) | не пиши ничего из этого — используй классы, которые уже всё умеют (string, vector, unique_ptr) |
Где что лучше
- Правило 0 — идеал. Большинство классов должны его использовать, потому что современные стандартные контейнеры сами всё умеют.
- Правило 3 — старый C++ или классы без move-семантики (легаси-код). Работает, но теряет оптимизации move.
- Правило 5 — правильный путь для классов, владеющих ресурсами (память, файлы, сокеты). Это твой случай — дерево владеет узлами в куче.
Почему у нас правило 5, а не 3
- Без move-конструктора
std::vector<MapTree>при росте будет копировать деревья (медленно, может быть в 100 раз дольше). С move — крадёт содержимое за O(1). - Без noexcept-move стандартные контейнеры всё равно выберут копирование. Поэтому
noexceptна move-операциях обязателен.
Что объединяет правило 5
Все пять методов отвечают на один вопрос: «как мой объект создаётся, копируется и уничтожается». Это полный жизненный цикл объекта. Они объединены тем, что обрабатывают владение ресурсами.
Где в твоём коде
// tree_base.h:370-418 (база)
OrderedTreeBase() noexcept(...); // 1. default-ctor
OrderedTreeBase(const OrderedTreeBase& o); // 2. copy-ctor
OrderedTreeBase(OrderedTreeBase&& o) noexcept(...); // 3. move-ctor
~OrderedTreeBase(); // 4. dtor
OrderedTreeBase& operator=(const OrderedTreeBase& o); // 5. copy=
OrderedTreeBase& operator=(OrderedTreeBase&& o) noexcept(...); // 6. move=
Связанные именованные требования
- CopyConstructible — есть копи-конструктор, создающий независимую копию.
- CopyAssignable — есть
operator=(const T&). - MoveConstructible — есть move-конструктор, оставляющий источник в «valid but unspecified» состоянии.
- MoveAssignable — есть
operator=(T&&). - Destructible — есть деструктор, корректно освобождающий ресурсы.
Это те самые имена, которые проверяет твой раздел TEST(NAMED_REQS, ...) в тестах.
2.4 this и как его скопировать без лямбды
Что такое this
this — это указатель на текущий объект внутри метода класса. В нестатических методах он всегда неявно есть. Через него ты обращаешься к полям и методам.
class A {
int x;
void show() {
std::cout << this->x; // явно
std::cout << x; // эквивалентно
}
};
Тип this
- В обычном методе:
A* - В const-методе:
const A*
Поэтому если ты в const-методе пытаешься менять поля — ошибка: «нельзя через const A*».
Где в твоей лабе видно
// tree_base.h:262 (CRTP-каст)
Derived& self() noexcept {
return *static_cast<Derived*>(this);
// this имеет тип OrderedTreeBase*,
// мы кастуем его до RBTree* (наследник),
// разыменовываем и возвращаем как ссылку
}
// tree_base.h:401 (защита от self-assignment)
OrderedTreeBase& operator=(const OrderedTreeBase& o) {
if (this != &o) { ... } // сравниваем адреса — мы не сами себе?
}
Как скопировать this без лямбды
Вопрос намекает на захват this в лямбде. Без лямбды есть три способа:
class A {
int x = 5;
void copy_self_1() {
A copy = *this; // 1) разыменовать и присвоить
// вызовется копи-конструктор
}
void copy_self_2() {
A copy(*this); // 2) явное конструирование
}
void copy_self_3() {
auto copy = this; // 3) скопировать УКАЗАТЕЛЬ
// (это не копия объекта, а копия адреса!)
}
};
Тут нюанс: «скопировать this» может означать две разные вещи:
- Скопировать сам объект (всё, на что указывает this):
A copy = *this; - Скопировать указатель:
A* ptr = this;
this — это указатель на текущий объект внутри его метода, неявно передающийся при каждом вызове нестатического метода. Чтобы скопировать сам объект — пишут auto copy = *this; (разыменование и вызов копи-конструктора). В лямбде C++17 для этого появился специальный синтаксис [*this], который захватывает копию объекта; без лямбды просто пишут *this.
2.5 Итераторы
Что такое и зачем
Итератор — это обобщённый «палец», указывающий на элемент контейнера. Через него STL-алгоритмы работают с любым контейнером, не зная его внутренней структуры. Это превращает проблему N×M (N контейнеров × M алгоритмов) в N+M.
Пять категорий — от слабого к сильному
| Категория | Что умеет | Где |
|---|---|---|
| Input (входной) | читать *it, идти ++it, одноразовый проход | istream_iterator |
| Output (выходной) | писать *it = x, идти ++it, одноразовый | ostream_iterator |
| Forward (однонаправленный) | + многократный проход | forward_list |
| Bidirectional (двунаправленный) | + --it | std::list, std::map, std::set, твой MapTree |
| Random Access (произвольный) | + it+5, it[i], it1 < it2 | vector, array, deque |
Какой у тебя — bidirectional
В коде это явно объявлено:
// tree_base.h:111
using iterator_category = std::bidirectional_iterator_tag;
Почему не random access? Потому что в дереве элементы НЕ лежат подряд в памяти. Прыгнуть на «5-й справа» нельзя за O(1) — пришлось бы делать 5 шагов.
Паспорт итератора
5 обязательных using-полей, по которым STL-алгоритмы понимают, что с итератором можно делать:
using iterator_category = std::bidirectional_iterator_tag;
using value_type = Value;
using difference_type = std::ptrdiff_t;
using reference = Value&; // или const Value& у const_iterator
using pointer = Value*;
Без них алгоритмы STL твой итератор не примут.
*, ->, ++ и (для bidirectional и выше) --. Пять категорий: input, output, forward, bidirectional, random access — от слабой к сильной. У моего MapTree итератор bidirectional, потому что дерево хранит элементы не подряд в памяти. Информация о категории указана в специальном поле iterator_category внутри класса — это «паспорт» для STL-алгоритмов, чтобы выбрать оптимальную стратегию работы.
2.6 Лямбды
Что это
Лямбда — это анонимный объект-функция, который можно объявить прямо в месте использования. Под капотом компилятор превращает её в класс с operator().
Полный синтаксис
[capture-list] (args) specifiers exception -> return-type { body }
- capture-list — что захватываем из окружения (по копии
[=], по ссылке[&], конкретные переменные). - args — аргументы, как у функции.
- specifiers — например,
mutable(разрешает менять захваченные по копии переменные). - exception — например,
noexcept. - return-type — обычно компилятор сам выводит (как
auto).
Лямбда == класс
// Лямбда:
[]() { std::cout << "Hello" << std::endl; }
// Компилятор разворачивает в:
class __Lambda1234 {
public:
auto operator()() const {
std::cout << "Hello" << std::endl;
}
};
У лямбды:
- Есть конструкторы по правилу 5.
- НЕТ операторов присваивания.
- Есть
operator()— вызов функции. - Если ничего не захватывает — можно сконвертировать в указатель на функцию.
Способы захвата
int i = 10;
[] {}; // без захвата
[i] {}; // i по копии
[&i] {}; // i по ссылке
[=] {}; // всё что использую — по копии
[&] {}; // всё — по ссылке
[=, &i] {}; // всё по копии, кроме i — по ссылке
[this] {}; // захватить указатель на объект
[*this] {}; // (C++17) захватить КОПИЮ объекта
Где можно использовать
- Аргумент алгоритма:
std::sort(v.begin(), v.end(), [](auto a, auto b){return a>b;}); - Предикат:
std::find_if,std::count_if, твойerase_if. - Колбэк в асинхронном коде, у потоков, в GUI.
- Локальная мини-функция внутри другой функции (как в
OrderedTreeBase::swap— лямбдаfixчинит указатели). - Сохранить в переменной через
std::function.
std::function — обёртка над лямбдой
std::function<int(int, int)> func;
func = flag ? [](int a, int b) { return a + b; }
: [](int a, int b) { return a - b; };
func(2, 3); // 5 или -1 в зависимости от флага
Лямбда в твоём коде
// tree_base.h:572 в swap
auto fix = [](NodeBase& h) noexcept {
if (!h.parent) h.left = h.right = &h;
else h.parent->parent = &h;
};
fix(header_);
fix(o.header_);
operator(). Состоит из списка захвата [], аргументов (), опциональных спецификаторов и тела {}. Используется где нужна короткая локальная функция: предикаты в алгоритмах, колбэки, разовая логика в swap. У меня в коде есть лямбда в swap базы — она чинит указатели parent у корней после обмена header'ов.
2.7 std::less, функторы, компараторы, предикаты
Что такое функтор (FunctionObject)
Это объект с перегруженным operator(), который можно вызвать как функцию. Это именованное требование C++ называется FunctionObject.
В STL есть готовые функторы в <functional>:
- Арифметические:
plus, minus, multiplies, divides, modulus, negate - Логические:
logical_and, logical_or, logical_not - Сравнения (предикаты):
equal_to, not_equal_to, less, greater, less_equal, greater_equal
Что такое предикат
Это функтор, возвращающий bool. Используется как «условие» в алгоритмах. std::less — бинарный предикат «меньше».
Что сравнивает std::less
Два значения через <. По умолчанию используется ассоциативными контейнерами для упорядочения ключей:
template <class T>
struct less {
bool operator()(const T& a, const T& b) const {
return a < b;
}
};
Особенности std::less
- Требует, чтобы у типа был
operator<. - Является «строгим частичным порядком» (strict weak ordering) — нужно для деревьев и сортировок.
- В C++14 появилась специализация
std::less<void>= transparent comparator, может сравнивать разные типы.
Пример своего компаратора
// Сравнение строк без учёта регистра
struct CaseInsensitiveLess {
bool operator()(const std::string& a, const std::string& b) const {
return std::lexicographical_compare(
a.begin(), a.end(),
b.begin(), b.end(),
[](char x, char y) {
return std::tolower(x) < std::tolower(y);
});
}
};
// Использование:
MapTree<std::string, int, RBTree, CaseInsensitiveLess> m;
m["Hello"] = 1;
m["hello"]; // тот же ключ, потому что регистр игнорируется
Использование std::greater
// сортировка по убыванию через предикат
std::sort(v.begin(), v.end(), std::greater<int>());
// твой тест с обратным MapTree
MapTree<int, int, RBTree, std::greater<int>> m;
operator<. Используется ассоциативными контейнерами по умолчанию для упорядочения ключей. Компаратор (или функтор сравнения) нужен, потому что не для всех типов есть осмысленный естественный порядок, и пользователь должен иметь возможность его задать. У меня в MapTree это четвёртый шаблонный параметр Compare с default-значением std::less<Key>. Можно подсунуть std::greater для обратного порядка или свой компаратор для нестандартного сравнения.
2.8 using, typedef, typename
typedef (старый способ)
typedef unsigned long long u64;
u64 x = 42;
using (новый способ, с C++11)
using u64 = unsigned long long;
u64 x = 42;
using лучше, потому что:
- Читается слева направо: «using имя = тип».
- Может быть шаблонным (typedef не может!):
template<typename T> using Vec = std::vector<T>;
using ещё используется для импорта имён из namespace
using cpp_lab::MapTree;
using std::cout;
using namespace std; // импортировать ВЕСЬ namespace (не рекомендуется в .h)
using для проброса методов в private-наследовании
class MapTree : private RBTree {
using base::begin; // делает begin публичным методом MapTree
using base::end;
};
typename — зачем
Используется внутри шаблонов, чтобы сказать компилятору: «вот это — тип, а не значение». Компилятор не может догадаться сам, потому что зависит от параметров шаблона.
template <typename Container>
void foo() {
typename Container::iterator it;
// без typename компилятор не знает,
// iterator — это тип или статическое поле
}
Где у тебя в коде
// MapTree.h:21-37 — массовый проброс из базы
using typename base::iterator;
using typename base::value_type;
using typename base::size_type;
// ...
// MapTree.h:53-78 — проброс методов
using base::begin;
using base::end;
using base::find;
// ...
typename T::iterator). У меня в MapTree десятки строк using typename base::... — это проброс всех стандартных typedef-ов наружу, чтобы контейнер был STL-совместимым.
2.9 BST, AVL, RB
BST (Binary Search Tree) — двоичное дерево поиска
Для любого узла: в левом поддереве — меньшие ключи, в правом — бо́льшие. Это правило выполняется рекурсивно для всех узлов.
[50]
/ \
[30] [70]
/ \ / \
[20] [40][60] [80]
Дает O(log n) поиск, вставку, удаление — при условии что дерево сбалансировано. Если вставлять ключи по возрастанию — дерево вырождается в список, и операции становятся O(n).
AVL-дерево
Названо по фамилиям советских математиков Адельсон-Вельский и Ландис (1962). Балансируется строго по высоте: разница высот левого и правого поддерева не больше 1.
- Хранит в узле:
int height - Высота ≤ 1.44·log(n+2) — почти идеально сбалансировано
- При вставке/удалении — балансируется через одинарные или двойные вращения
- Поиск чуть быстрее RB; вставка/удаление медленнее
- Применение: базы данных, индексы с редкими изменениями
Red-Black tree (красно-чёрное дерево)
Балансируется через цвета (Байер 1972, Гибас-Седжвик 1978).
5 правил RB:
- Каждый узел красный или чёрный.
- Корень всегда чёрный.
- NIL-листы (
nullptr) считаются чёрными. - У красного узла НЕТ красных детей (нет двух красных подряд).
- От любого узла до его NIL-потомков на всех путях — одинаковое число чёрных узлов.
50 / \ 30 70 / \ / \ 20 40 60 80
- Хранит в узле:
bool red - Высота ≤ 2·log(n+1) — чуть менее идеально
- Меньше вращений → быстрее вставка/удаление
- Применение: std::map, std::set, JVM TreeMap, ядро Linux (планировщик CFS)
Сравнение
| AVL | RB | |
|---|---|---|
| Хранит | int height | bool red |
| Высота | ≤ 1.44·log n | ≤ 2·log n |
| Поиск | чуть быстрее | чуть медленнее |
| Вставка/удаление | дороже | дешевле |
| Стиль кода у тебя | рекурсивный | итеративный |
2.10 Все способы удалить узел в BST
Три классических случая. Тот же алгоритм применяется в AVL и в RB (с последующей балансировкой).
Случай 1 — у узла НЕТ детей (лист)
до: после удаления [40]:
[30] [30]
/ \ /
[20] [40] [20]
Просто отцепляем узел от родителя, родителю выставляем nullptr в соответствующее поле.
Случай 2 — у узла ОДИН ребёнок
до: после удаления [40]:
[30] [30]
/ \ / \
[20] [40] [20] [45]
\
[45]
Ребёнок поднимается на место удаляемого узла.
Случай 3 — у узла ДВА ребёнка (сложный)
Нельзя просто отцепить — куда тогда деть детей? Находим successor — минимум правого поддерева (это «следующий по порядку» ключ). Заменяем удаляемый узел на него.
до: после удаления [50]:
[50] [60]
/ \ successor / \
[30] [70] это [60] [30] [70]
/ \ (min справа) \
[60] [80] [80]
Где в твоём коде
- RBTree: в
unlink_and_fix— все три случая в одной функции через переменныеyиx. Если узел чёрный — добавочный цикл с «двойным чёрным». - AVLTree: в
erase_target. Случаи 1-2 — одной строкой (возвращает уцелевшего ребёнка). Случай 3 — черезerase_minдля поиска successor + перенос.
Дополнительный сценарий — extract
В C++17 есть extract: узел отцепляется от структуры, но НЕ удаляется физически. Его можно положить в другой контейнер через node_handle. У тебя это делается через detach — тот же unlink_and_fix, но без delete.
3Любимые вопросы Богдана
lvalue — выражение, имеющее идентифицируемое место в памяти (имеет имя или адрес).
int x = 5; // x — lvalue
std::string s; // s — lvalue
arr[3]; // элемент массива — lvalue
foo(); // если foo возвращает T& — lvalue
rvalue — временное значение без долговременной идентичности.
42; // литерал — prvalue
x + 1; // результат арифметики — prvalue
foo(); // если foo возвращает T — prvalue
std::move(x); // xvalue (rvalue с именем)
Тест: можешь сделать & от выражения — lvalue, не можешь — rvalue.
Иерархия узлов в tree_base.h:16-55:
struct NodeBase { // базовый — только связи
NodeBase* parent = nullptr;
NodeBase* left = nullptr;
NodeBase* right = nullptr;
bool is_header = false;
};
template<typename Value>
struct Node : NodeBase { // + поле value
Value value; // pair<const Key, T>
};
template<typename Value>
struct RBNode : Node<Value> { // + цвет
bool red = true;
};
template<typename Value>
struct AVLNode : Node<Value> { // + высота
int height = 1;
};
Узлы хранятся в куче, создаются через new tree_node(...) в emplace и удаляются через delete в erase_one. Дерево владеет указателями на эти узлы через header_.parent (корень) и через parent/left/right у каждого узла.
Правило 3 (старое, C++98): деструктор + копи-ctor + копи=. Этого хватало в эпоху без move-семантики.
Правило 5 (C++11+): + move-ctor + move=. Добавлено, потому что появилась move-семантика — оптимизация для временных объектов.
Где что:
- Современный C++ (твоя лаба): правило 5. Без него
std::vector<MapTree>при росте будет копировать вместо перемещения. - Совсем старый легаси-код: правило 3 — хотя бы безопасно.
- Классы без управления ресурсами: правило 0 — пусть компилятор сам генерит.
Все пять методов отвечают за жизненный цикл и владение ресурсами:
- Деструктор — освобождает ресурсы.
- Копи-ctor — создаёт независимую копию.
- Копи= — заменяет содержимое копией.
- Move-ctor — крадёт ресурсы из временного.
- Move= — заменяет содержимое, крадя ресурсы.
Если хотя бы один написан вручную (например, есть деструктор) — все остальные сгенерированные компилятором по умолчанию будут НЕКОРРЕКТНЫМИ для классов с ресурсами: они сделают shallow copy, оба объекта будут владеть одной памятью, и при разрушении произойдёт double-free.
Что объединяет: ответ на вопрос «как мой объект создаётся, копируется, перемещается и уничтожается».
this — указатель на текущий объект внутри его метода. Неявно передаётся при каждом вызове нестатического метода. Тип: A* в обычном методе, const A* в const-методе.
В твоём коде используется в:
- CRTP-касте:
static_cast<Derived*>(this) - Защите от self-assignment:
if (this != &o) - Обращении к полям базы:
this->header_
Если речь про копирование самого объекта:
A copy = *this; // разыменовать и присвоить → копи-ctor
A copy(*this); // явное конструирование
Если речь про копирование указателя:
A* ptr = this; // просто копия указателя
В лямбде C++17 есть синтаксис [*this] для захвата копии объекта. Без лямбды эквивалент — *this в любом контексте, где ждут объект.
Шаблон сам по себе НЕ накладывает требований — это просто рецепт. Требования возникают в момент инстанцирования: тип должен поддерживать все операции, которые шаблон использует.
Например, для моего MapTree<Key, T, Tree, Compare>:
Key— должен поддерживатьCompare(Key, Key), обычноoperator<.T— должен быть default-constructible дляoperator[], copy-constructible для копи-операций.Compare— должен быть FunctionObject сoperator()(Key, Key) -> bool(строгий частичный порядок).
В C++20 эти требования можно явно объявить через concepts (requires). В моей лабе требования проверяются именованными требованиями через тесты NAMED_REQS: DefaultConstructible, CopyConstructible, CopyAssignable, MoveConstructible, MoveAssignable, Destructible, Swappable.
Двойной амперсант && — это rvalue-ссылка. Она привязывается ТОЛЬКО к rvalue (временным значениям).
Зачем нужна: чтобы перегружать функции по категории значения:
void foo(const std::string& s); // для lvalue → копируем
void foo(std::string&& s); // для rvalue → крадём
foo(name); // 1-я перегрузка (name — lvalue)
foo("hello"); // 2-я перегрузка (литерал — prvalue)
foo(std::move(name)); // 2-я перегрузка (xvalue)
Это основа move-семантики: для временных можно использовать оптимизированный путь, где содержимое переносится, а не копируется.
Особый случай: в шаблонах T&& — это forwarding reference (универсальная ссылка), она принимает И lvalue, И rvalue, и сохраняет категорию для дальнейшей передачи.
Зачем: один и тот же алгоритм нужен для разных типов (int, double, string), и переписывать его N раз — глупо. Шаблон позволяет написать ОДИН раз, и компилятор сгенерирует версию под каждый тип.
Что это: рецепт кода. Сам шаблон НЕ компилируется в машинный код. Машинный код появляется только когда ты используешь шаблон с конкретными типами.
Виды параметров шаблона:
- Тип:
template<typename T>илиtemplate<class T>(одинаково). - Константа компиляции (non-type):
template<int N>,template<bool Flag>. - Шаблон шаблона:
template<template<typename> class Container>. - Variadic:
template<typename... Args>.
В моей лабе используются все четыре:
Key, T, Compare— типы;bool IsConstв TreeIterator — константа;template<typename, typename, typename> class Tree— шаблон шаблона;Args... argsв emplace — variadic.
- Предикат в STL-алгоритмах:
std::sort(v.begin(), v.end(), [](auto a, auto b){return a>b;}) - Колбэк: в потоках, асинхронном коде, GUI.
- Локальная мини-функция: когда нужна функция только в одном месте.
- Сохранение в переменной через
std::functionилиauto. - Замена функтора: вместо struct с operator() пишут
[](){}. - Передача поведения: например, в твой
erase_if(c, pred)—predчаще всего лямбда.
Лямбда имеет:
- capture-list — список захваченных переменных из окружения (по копии, по ссылке, this).
- args — аргументы как у обычной функции.
- specifiers —
mutable,constexpr, ... - exception-specification —
noexcept. - return-type — после
->, обычно опускается. - body — тело функции.
Под капотом компилятор разворачивает её в класс с operator():
// лямбда
int y = 10;
auto f = [y](int x) { return x + y; };
// компилятор разворачивает в:
class __Lambda1234 {
int y;
public:
__Lambda1234(int y_) : y(y_) {}
int operator()(int x) const { return x + y; }
};
Лямбда:
- есть конструкторы по правилу 5,
- НЕТ операторов присваивания,
- есть оператор вызова
(), - (C++20) конструктор по умолчанию, если ничего не захватывает,
- если ничего не захватывает — может быть сконвертирована в указатель на функцию.
std::less<T> сравнивает два значения типа T через operator< и возвращает bool.
Своя реализация:
template <typename T>
struct my_less {
bool operator()(const T& a, const T& b) const {
return a < b;
}
};
// Использование с std::sort:
std::vector<int> v = {5, 2, 8, 1};
std::sort(v.begin(), v.end(), my_less<int>());
// результат: 1, 2, 5, 8
В моей лабе MapTree получает Compare как 4-й шаблонный параметр со значением по умолчанию std::less<Key>.
Особенности std::less:
- Требует, чтобы у типа был
operator<. - Должен быть strict weak ordering — строгий частичный порядок:
- irreflexive:
!less(a, a), - asymmetric:
less(a, b) ⇒ !less(b, a), - transitive:
less(a, b) && less(b, c) ⇒ less(a, c).
- irreflexive:
- C++14: специализация
std::less<void>— transparent comparator, сравнивает значения разных типов.
Свой компаратор для сравнения по длине строки:
struct ByLength {
bool operator()(const std::string& a, const std::string& b) const {
return a.size() < b.size();
}
};
MapTree<std::string, int, RBTree, ByLength> m;
m["a"] = 1; // длина 1
m["hi"] = 2; // длина 2
m["hello"] = 3; // длина 5
// упорядочено по длине ключа
typedef — старый способ дать псевдоним типу. typedef unsigned long long u64;
using — новый способ (C++11), читается слева направо, поддерживает шаблоны. using u64 = unsigned long long;
typename — два смысла:
- В параметрах шаблона:
template<typename T>— то же чтоtemplate<class T>. - Внутри шаблонов перед именем, зависящим от параметра, — чтобы сказать «это тип»:
template<typename Container> void foo() { typename Container::iterator it; // без typename компилятор не знает, // iterator — это тип или поле }
В моей лабе обе формы массово используются: using base::iterator для проброса методов, using typename base::iterator для проброса типа.
Это две разные вещи, легко путать.
Шаблонный класс — это класс, имеющий шаблонные параметры:
template<typename T>
class vector { ... };
vector<int> v;
Класс как шаблонный тип (template template parameter) — это когда сам шаблон передаётся как параметр другого шаблона:
template<template<typename> class Container>
class Adapter {
Container<int> data; // Container — это шаблон,
// его ещё надо инстанцировать
};
Adapter<vector> a; // vector — шаблон, не конкретный тип
В моей лабе MapTree принимает Tree как template template parameter:
template<typename Key, typename T,
template<typename, typename, typename> class Tree,
typename Compare = std::less<Key>>
class MapTree : private Tree<Key, T, Compare> { ... };
Это нужно, чтобы MapTree сам решал, с какими типами инстанцировать дерево, а не получал готовый RBTree<int, string>.
Шаблон существует ТОЛЬКО на этапе компиляции. Реального символа в объектном файле у самого шаблона нет.
Когда в коде встречается RBTree<int, std::string> — компилятор:
- Проверяет, не инстанцировал ли он уже этот класс с такими параметрами в текущем .cpp.
- Если нет — генерирует ВЕСЬ код этого класса прямо в текущий объектный файл, помечая символы как weak (слабые).
Если в a.cpp и b.cpp оба используют RBTree<int, string> — оба сгенерируют его код. На этапе линковки линкер видит дубликаты weak-символов и оставляет один (это «COMDAT folding» или «vague linkage»).
Поэтому шаблоны живут в .h-файлах: иначе один .cpp не увидит определение шаблона и не сможет его инстанцировать.
Альтернатива — явная инстанциация в .cpp:
// max.cpp
template<> int max<int>(int a, int b);
template<> float max<float>(float a, float b);
Тогда определения этих экземпляров есть в одном .cpp, и остальные .cpp могут линковаться с ним.
В обычном BST три случая:
1. Лист (0 детей) — просто отцепить от родителя.
[30] [30]
/ \ → /
[20] [40] [20] (удалили 40)
2. Один ребёнок — ребёнок поднимается на место удалённого.
[30] [30]
/ \ / \
[20] [40] → [20] [45] (удалили 40)
\
[45]
3. Два ребёнка — находим successor (минимум правого поддерева), переносим его на место удаляемого.
[50] [60]
/ \ / \
[30] [70] → [30] [70] (удалили 50)
/ \ \
[60] [80] [80]
Дополнительно для AVL/RB — после удаления нужна балансировка (вращения и/или перекраски).
Есть ещё extract — узел отцепляется, но НЕ удаляется физически. Возвращается в node_handle для дальнейшего использования.
Три применения:
1. Псевдоним типа (вместо typedef):
using u64 = unsigned long long;
using Map = std::map<int, std::string>;
2. Импорт имени из namespace:
using std::cout;
using cpp_lab::MapTree;
3. Проброс методов базы (при private-наследовании):
class MapTree : private RBTree {
using base::begin; // делает begin публичным
};
В моей лабе все три применения. Самое массовое — проброс методов из OrderedTreeBase наружу через MapTree.
Итератор — обобщённый «палец», указывающий на элемент контейнера. Универсальный интерфейс обхода.
5 категорий:
- Input — только чтение, только вперёд, один проход. Пример:
istream_iterator(чтение из файла). - Output — только запись, только вперёд, один проход. Пример:
ostream_iterator(запись в файл). - Forward — чтение + запись, только вперёд, многократный проход. Пример:
std::forward_list. - Bidirectional — +
--it. Пример:std::list,std::map, мой MapTree. - Random Access — + произвольный прыжок
it + N. Пример:std::vector.
Зачем нужны: один и тот же STL-алгоритм (sort, find, count) работает с любым контейнером, у которого есть подходящие итераторы. Это решает проблему N×M (N контейнеров × M алгоритмов).
Стандартный функтор-предикат в <functional>. Реализует операцию «меньше» через operator<:
template <class T>
struct less {
bool operator()(const T& a, const T& b) const {
return a < b;
}
};
Используется ассоциативными контейнерами (std::map, std::set, мой MapTree) для упорядочения ключей по умолчанию.
Оба — самобалансирующиеся двоичные деревья поиска. Гарантируют O(log n) для поиска/вставки/удаления.
AVL (Адельсон-Вельский, Ландис, 1962):
- Балансируется по высоте: разница высот соседних поддеревьев ≤ 1.
- Хранит в узле
int height. - Высота дерева ≤ 1.44·log(n).
- Чуть быстрее поиск, медленнее вставка/удаление.
RB-tree (Байер 1972, Гибас-Седжвик 1978):
- Балансируется через 5 цветовых правил.
- Хранит в узле
bool red. - Высота ≤ 2·log(n).
- Меньше вращений → быстрее вставка/удаление.
- Используется в std::map, std::set, JVM TreeMap, ядре Linux.
Компаратор — функтор сравнения, принимающий два значения и возвращающий bool (или enum, как у C++20 spaceship). В контексте ассоциативных контейнеров — задаёт порядок ключей.
Почему нужен:
- Не для всех типов есть осмысленный естественный порядок.
- Иногда нужно нестандартное упорядочение (по убыванию, без учёта регистра, по части ключа).
- Пользователь должен иметь возможность задать своё правило, не меняя контейнер.
В моей лабе MapTree принимает Compare 4-м шаблонным параметром, по умолчанию std::less<Key>. Хранится в базе как поле compare_ и применяется во всех операциях, требующих порядка (find, lower_bound, BST-спуск при вставке).
CRTP = Curiously Recurring Template Pattern = «странно рекурсивный шаблонный паттерн».
Это конструкция, при которой базовый класс шаблонизирован по имени своего наследника:
template<typename Derived>
class Base {
void api() {
static_cast<Derived*>(this)->impl();
// вызов метода наследника без virtual
}
};
class RBTree : public Base<RBTree> {
void impl() { ... }
};
Зачем: статический полиморфизм без runtime-стоимости virtual-функций. База может вызывать методы наследника, компилятор инлайнит вызовы.
У меня OrderedTreeBase<Derived,...> вызывает self().emplace, self().erase_one, self().detach — это методы наследника (RBTree или AVLTree).
Экземпляр класса — объект, созданный по чертежу-классу.
В шаблонах есть похожий, но другой термин: инстанциация шаблона — генерация конкретного класса/функции из шаблона при подстановке типов.
Пример:
std::vector— шаблон.std::vector<int>— инстанциация шаблона (конкретный класс).std::vector<int> v;—vэто экземпляр классаstd::vector<int>.
namespace — «пространство имён», папка для имён классов/функций. Без него имена из разных библиотек могли бы конфликтовать.
У меня в коде cpp_lab::tree_detail — вложенный namespace:
cpp_lab— мой проект.tree_detail— внутренняя кухня, не для пользователя.
Соглашение C++: всё, что не предназначено для прямого использования, кладётся в namespace с суффиксом _detail или _impl. Это сигнал «не трогай снаружи, могу сломать совместимость».
RB-tree даёт лучший баланс работы и качества для общего случая со смешанными операциями (вставки, удаления, поиски).
- AVL даёт более компактное дерево (поиск быстрее), но требует больше вращений при каждом изменении.
- RB допускает чуть более «кривое» дерево (поиск чуть медленнее), но изменения дешевле.
На практике большинство приложений делают смешанные операции, поэтому RB выигрывает. AVL применяют там, где много поисков и мало изменений — например, индексы в базах данных.
Гарантии корректности кода при возникновении исключений. Четыре уровня:
- No-throw — функция гарантированно не бросит. Помечается
noexcept. Пример: swap, деструкторы, size(). - Strong (сильная) — либо операция выполнена полностью, либо состояние объекта НЕ ИЗМЕНИЛОСЬ. Пример: insert в std::map.
- Basic (базовая) — объект остаётся в валидном состоянии, инварианты сохранены, но содержимое может измениться.
- None — UB, утечки, что угодно.
В моей лабе:
- insert/emplace — strong: если new упал, дерево не тронуто.
- copy-constructor — strong через try/catch + clear на исключении.
- operator= — strong через copy-and-swap идиому.
- swap, move, clear, destructor — no-throw (помечены noexcept).
emplace создаёт объект прямо внутри контейнера, без промежуточных копий.
Сравнение:
// insert:
m.insert({1, "hello"});
// 1. Создаётся ВРЕМЕННАЯ пара pair<int, string>{1, "hello"}
// 2. Передаётся в insert
// 3. Внутри копируется/перемещается в узел
// emplace:
m.emplace(1, "hello");
// 1. Выделяется память под узел
// 2. Конструктор pair вызывается ПРЯМО в узле через perfect forwarding
Для маленьких типов разница незаметна. Для unique_ptr (некопируемый!) или больших строк — критично.
extract (C++17) — выдёргивает узел из контейнера, не уничтожая значение. Возвращает node_handle.
Зачем: переместить узел между контейнерами без копирования значения. Особенно ценно для типов, которые нельзя копировать (например, unique_ptr).
std::map<int, std::unique_ptr<Big>> mA, mB;
mA[1] = std::make_unique<Big>();
auto nh = mA.extract(1); // узел вынут
nh.key() = 2; // можно переименовать!
mB.insert(std::move(nh)); // узел в mB, значение не копировано
Перфект-форвардинг — это механизм, позволяющий функции-обёртке передать свои аргументы дальше, сохраняя их категорию (lvalue/rvalue).
template<typename... Args>
void wrapper(Args&&... args) { // forwarding reference
real_func(std::forward<Args>(args)...);
// если был lvalue — передастся как lvalue
// если был rvalue — передастся как rvalue
}
В моём emplace это используется — все аргументы передаются в конструктор value_type без потери эффективности.
Это атрибут C++17. Атрибуты в C++ обозначаются двойными квадратными скобками. Они — «подсказки компилятору», не меняют поведение программы.
[[nodiscard]] заставляет компилятор выдавать предупреждение, если результат функции проигнорирован:
[[nodiscard]] bool is_valid();
is_valid(); // warning: result ignored
if (is_valid()) {} // OK
В моей лабе стоит на empty() — защита от типичной ошибки m.empty(); (думали что очистили, а на самом деле просто проверили и забыли).
ADL = Argument-Dependent Lookup = «поиск, зависящий от аргумента» (поиск Кёнига). Правило, по которому компилятор ищет функцию.
При вызове foo(x) без квалификатора компилятор ищет foo не только в текущем namespace, но и в namespace типа аргумента x.
Зачем: позволяет иметь оптимизированные версии функций (особенно swap) рядом с типом, и стандартные алгоритмы их найдут:
namespace mylib {
struct Foo {};
void swap(Foo& a, Foo& b) { /* быстрый */ }
}
mylib::Foo a, b;
{
using std::swap;
swap(a, b); // ADL найдёт mylib::swap, а не std::swap
}
У меня friend void swap(node_handle&, node_handle&) и аналогичная свободная функция для MapTree — обе сделаны как ADL-friendly.
Оба двигают итератор на один шаг вперёд. Разница в том, что возвращают.
++it (префиксный) — двигает И возвращает себя (уже сдвинутого).
it++ (постфиксный) — запоминает старое состояние, двигает себя, возвращает СТАРОЕ.
int a = 5;
int b = a++; // b = 5 (старое), a = 6
int x = 5;
int y = ++x; // x = 6, y = 6 (новое)
Постфиксный делает копию объекта (для возврата старого). Для итераторов дерева — это копирование указателя, не страшно. Но привычка: пиши ++it если возвращаемое значение не нужно.
4Ключевые места кода
4.1 Где хранятся узлы дерева
Узлы хранятся в куче, создаются через new, удаляются через delete. Дерево хранит указатели через header_ и через parent/left/right внутри узлов.
// БАЗА — только связи
struct NodeBase {
NodeBase* parent = nullptr;
NodeBase* left = nullptr;
NodeBase* right = nullptr;
bool is_header = false;
};
// + ЗНАЧЕНИЕ
template<typename Value>
struct Node : NodeBase {
Value value; // pair<const Key, T>
};
// + ЦВЕТ (для RB)
template<typename Value>
struct RBNode : Node<Value> {
bool red = true;
};
// + ВЫСОТА (для AVL)
template<typename Value>
struct AVLNode : Node<Value> {
int height = 1;
};
class OrderedTreeBase {
protected:
NodeBase header_{}; // фейковый узел:
// header_.parent = корень дерева
// header_.left = минимум
// header_.right = максимум
// header_.is_header = true
size_type size_ = 0;
Compare compare_{};
};
template<typename... Args>
std::pair<iterator, bool> emplace(Args&&... args) {
auto* z = new tree_node(std::forward<Args>(args)...);
// ↑ узел в куче, конструктор value вызывается прямо в нём
auto r = reinsert(z);
if (!r.second) delete z;
return r;
}
iterator erase_one(iterator pos) noexcept {
iterator next = pos;
++next;
delete static_cast<tree_node*>(unlink_and_fix(pos.node_));
// ↑ каст обязателен, чтобы delete вызвал правильный dtor
--this->size_;
return next;
}
4.2 Ключевые места кода
CRTP-каст
Derived& self() noexcept {
return *static_cast<Derived*>(this);
}
// База кастит this до Derived* и вызывает методы наследника.
// Это даёт статический полиморфизм без virtual.
Защита от self-assignment
OrderedTreeBase& operator=(const OrderedTreeBase& o) {
if (this != &o) { // мы не сами себе?
Derived tmp(static_cast<const Derived&>(o));
self().swap(tmp);
}
return *this;
}
Strong exception safety в копи-конструкторе
OrderedTreeBase(const OrderedTreeBase& o) : compare_(o.compare_) {
init_header();
if (o.size_ == 0) return;
try {
set_root(copy_subtree(o.header_.parent, header_ptr()));
refresh_extremes();
size_ = o.size_;
} catch (...) {
clear(); // очистить что успели
throw; // пробросить дальше
}
}
SFINAE-конструктор конвертации iterator → const_iterator
template<bool C = IsConst, std::enable_if_t<C, int> = 0>
TreeIterator(const TreeIterator<Value, false>& o) noexcept
: node_(o.node_) {}
// Этот конструктор существует ТОЛЬКО когда IsConst=true.
// Разрешает iterator → const_iterator, запрещает обратное.
Вращения в RB
static void rotate_left(NodeBase* x, NodeBase*& root) noexcept {
NodeBase* y = x->right;
x->right = y->left;
if (y->left) y->left->parent = x;
y->parent = x->parent;
if (x == root) root = y;
else if (x == x->parent->left) x->parent->left = y;
else x->parent->right = y;
y->left = x;
x->parent = y;
}
Восстановление RB-правил после вставки
while (z != root && red_of(z->parent)) {
NodeBase* parent = z->parent;
NodeBase* grand = parent->parent;
const bool pl = parent == grand->left;
NodeBase* uncle = pl ? grand->right : grand->left;
if (red_of(uncle)) { // случай 1: красный дядя
paint(parent, false);
paint(uncle, false);
paint(grand, true);
z = grand; // проблема всплыла наверх
} else {
if (pl ? (z == parent->right) : (z == parent->left)) {
// случай 2: внутренний — выпрямить вращением
z = parent;
pl ? rotate_left(z, root) : rotate_right(z, root);
parent = z->parent;
grand = parent->parent;
}
// случай 3: внешний — вращение деда + перекраска
paint(parent, false);
paint(grand, true);
pl ? rotate_right(grand, root) : rotate_left(grand, root);
}
}
paint(root, false); // корень всегда чёрный
AVL — рекурсивная балансировка
static NodeBase* rebalance(NodeBase* n) noexcept {
update_h(n);
const int b = bf(n); // баланс-фактор
if (b > 1) { // перевес влево
if (bf(n->left) < 0) { // зигзаг → двойное вращение
n->left = rotate_left(n->left);
n->left->parent = n;
}
return rotate_right(n);
}
if (b < -1) { // перевес вправо
if (bf(n->right) > 0) {
n->right = rotate_right(n->right);
n->right->parent = n;
}
return rotate_left(n);
}
return n;
}
5Доп. задачи · что могут попросить написать
5.1 Факториал на шаблонах
Это прямо из лекции 04.15. Используются шаблонные аргументы-константы (non-type template arguments). Факториал считается во время компиляции.
template<int Value>
int getFactorial() {
return Value * getFactorial<Value - 1>();
}
template<> int getFactorial<1>() { // база рекурсии
return 1;
}
int main() {
const int result = getFactorial<5>(); // = 120
// ↑ вычислено компилятором, в runtime ничего не считается
return result;
}
template<unsigned N>
struct Factorial {
static constexpr unsigned long long value
= N * Factorial<N - 1>::value;
};
template<>
struct Factorial<0> {
static constexpr unsigned long long value = 1;
};
// использование:
constexpr auto f5 = Factorial<5>::value; // 120 на этапе компиляции
constexpr unsigned long long factorial(unsigned n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
constexpr auto f5 = factorial(5); // compile-time
int n = 10;
auto f10 = factorial(n); // runtime
5.2 Гробовая таска — метапрограммирование
Скорее всего попросят написать что-то вычисляющееся на этапе компиляции. Вот несколько штук, любая может прокатить:
Числа Фибоначчи
template<unsigned N>
struct Fib {
static constexpr unsigned long long value
= Fib<N - 1>::value + Fib<N - 2>::value;
};
template<> struct Fib<0> { static constexpr unsigned long long value = 0; };
template<> struct Fib<1> { static constexpr unsigned long long value = 1; };
// Fib<10>::value == 55
Степень числа
template<int Base, unsigned Exp>
struct Pow {
static constexpr long long value = Base * Pow<Base, Exp - 1>::value;
};
template<int Base> struct Pow<Base, 0> {
static constexpr long long value = 1;
};
// Pow<2, 10>::value == 1024
Проверка типа на этапе компиляции (свой is_same)
template<typename T, typename U>
struct is_same {
static constexpr bool value = false;
};
template<typename T>
struct is_same<T, T> {
static constexpr bool value = true;
};
// is_same<int, int>::value == true
// is_same<int, double>::value == false
Сумма пакета (variadic)
template<int... Vals>
struct Sum;
template<>
struct Sum<> {
static constexpr int value = 0;
};
template<int First, int... Rest>
struct Sum<First, Rest...> {
static constexpr int value = First + Sum<Rest...>::value;
};
// Sum<1, 2, 3, 4, 5>::value == 15
5.3 Другие мини-таски, связанные с темой
А) Свой компаратор для MapTree
Если попросят «сделай MapTree с нестандартным сравнением» — пиши struct с operator() и передавай 4-м параметром:
struct ByAbs {
bool operator()(int a, int b) const {
return std::abs(a) < std::abs(b);
}
};
MapTree<int, std::string, RBTree, ByAbs> m;
m[-5] = "five";
m[3] = "three";
m[-1] = "one";
// порядок обхода: -1, 3, -5 (по модулю)
Б) Подсчёт количества элементов больше N
template<typename Map>
int count_greater_than(const Map& m, int threshold) {
int n = 0;
for (const auto& [k, v] : m) {
if (v > threshold) ++n;
}
return n;
}
В) Печать дерева в порядке возрастания
template<typename Map>
void print_sorted(const Map& m) {
for (const auto& [k, v] : m) {
std::cout << k << " → " << v << "\n";
}
// порядок гарантирован свойством BST + in-order обходом
}
Г) Слияние двух MapTree без копий значений (через extract)
template<typename Map>
void move_all(Map& src, Map& dst) {
while (!src.empty()) {
auto nh = src.extract(src.begin());
dst.insert(std::move(nh));
}
}
// или ещё короче:
dst.merge(src); // у меня в коде уже есть merge
Д) Вектор ключей
template<typename Map>
auto keys(const Map& m) {
std::vector<typename Map::key_type> result;
result.reserve(m.size());
for (const auto& [k, v] : m) result.push_back(k);
return result;
}
Е) Поменять значения местами
template<typename Map>
void swap_values(Map& m, const typename Map::key_type& a,
const typename Map::key_type& b) {
auto ita = m.find(a);
auto itb = m.find(b);
if (ita != m.end() && itb != m.end()) {
std::swap(ita->second, itb->second);
// КЛЮЧИ менять нельзя (они const) — только значения
}
}
Ж) Печать структуры дерева (для отладки)
// Если попросят добавить вспомогательный метод —
// можно дописать в RBTree приватный метод для отладки:
void print_tree(NodeBase* node, int depth = 0) {
if (!node) return;
print_tree(node->right, depth + 1);
for (int i = 0; i < depth; ++i) std::cout << " ";
std::cout << value_of(node).first
<< (red_of(node) ? "R" : "B") << "\n";
print_tree(node->left, depth + 1);
}
З) Свой алгоритм через итераторы
// Подсчитать сумму значений в диапазоне ключей [lo, hi]
template<typename Map>
auto range_sum(const Map& m,
const typename Map::key_type& lo,
const typename Map::key_type& hi) {
typename Map::mapped_type sum{};
auto it = m.lower_bound(lo);
auto end = m.upper_bound(hi);
for (; it != end; ++it) sum += it->second;
return sum;
}
∞Чек-лист перед защитой
- ✓ Знаю все 5 категорий значений и могу пример каждой
- ✓ Объясню правило 5 и зачем оно нужно для моего класса
- ✓ Покажу где хранятся узлы (NodeBase → Node → RBNode/AVLNode + new в куче)
- ✓ Объясню что такое CRTP и зачем его использую
- ✓ Расскажу про bidirectional итератор и его паспорт
- ✓ Покажу 3 случая удаления в BST и нарисую их
- ✓ Сравню AVL и RB по конкретным метрикам
- ✓ Объясню что такое std::less и напишу свой компаратор
- ✓ Знаю что такое лямбда и как она разворачивается в класс
- ✓ Расскажу про exception safety и где у меня strong
- ✓ Объясню разницу между using, typedef, typename
- ✓ Напишу факториал на шаблонах если попросят
- ✓ Знаю как шаблоны работают при компиляции и линковке