ЛР4 STL · MapTree Начало

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"})

Разберу весь путь по шагам — от самого вызова до того, что в итоге вернётся пользователю.

1
Пользователь вызывает
main.cpp
m.insert({1, "hello"}) — создаётся временный std::pair<const int, std::string> (это rvalue) и передаётся в insert.
2
MapTree::insert
MapTree.h · using base::insert
У MapTree метод insert унаследован от OrderedTreeBase через директиву using. Просто открывает доступ к base-методу.
3
OrderedTreeBase::insert(value_type&&)
tree_base.h:448
Видит, что параметр — rvalue, выбирается перегрузка с &&. Вызывает self().emplace(std::move(v)).
4
self() — CRTP-каст
tree_base.h:262
База кастит this к RBTree* и вызывает emplace уже у наследника. Никаких virtual — всё на этапе компиляции.
5
RBTree::emplace(Args&&...)
RBTree.h:292
new tree_node(std::forward<Args>(args)...) — создаёт RBNode<pair> в куче, конструктор пары вызывается прямо в узле (perfect forwarding).
6
RBTree::reinsert(z)
RBTree.h:222
BST-спуск от корня вниз сравнениями compare_(key, node.key). Если ключ уже есть — узел удаляется, возвращается (it, false).
7
link_and_fix(insert_left, z, p)
RBTree.h:64
Физическая привязка: p.left/right = z. Новый узел красят красным. Цикл восстанавливает правила RB: пока есть «два красных подряд» — перекраски и вращения.
8
++size_, корень всегда чёрный
RBTree.h:115
В конце paint(root, false) — корень принудительно чёрный (правило 2 RB-дерева).
9
Возврат iterator + bool
→ к пользователю
Пользователь получает std::pair<iterator, bool>: куда вставили и был ли вставлен новый элемент.

1.2 Полный путь m[5] = "hello"

1
m[5]
MapTree.h:96 · operator[](const Key&)
Зовёт try_emplace(key) и возвращает ссылку на second. Если ключа не было — создаст пару с default-конструированным значением.
2
try_emplace(key)
MapTree.h:101
base::find(key) — если ключ есть, значение НЕ создаётся (экономия для тяжёлых T). Если нет — base::emplace(piecewise_construct, ...).
3
piecewise_construct
std::piecewise_construct из <tuple>
Тэг для конструктора pair — «не пытайся скопировать пару, построй ключ из 1-го кортежа, значение из 2-го». Нужен потому что pair имеет несколько перегрузок.
4
… дальше как при insert
RBTree::emplace → reinsert → link_and_fix → возврат iterator.
5
.first->second
Берём ссылку на значение из итератора и возвращаем её. На месте вызова происходит = "hello" — присваивание по ссылке.

1.3 Полный путь m.erase(5)

1
OrderedTreeBase::erase(const Key&)
tree_base.h:496
find(key), если не нашли — возвращаем 0. Если нашли — self().erase_one(it).
2
RBTree::erase_one
RBTree.h:245
Сохраняет «следующий итератор» (требование std::map), вызывает unlink_and_fix, делает delete, уменьшает size.
3
unlink_and_fix
RBTree.h:117
Три случая: 0 детей / 1 ребёнок / 2 ребёнка (через successor). Если удалили чёрный — запуск цикла «двойного чёрного» с вращениями и перекрасками.
4
Возврат: size_type
Возвращаем 1 (удалили). Пользователь получает количество удалённых элементов.

2Теория · то, что реально гоняют на защите

2.1 Шаблоны (templates)

Что это. Шаблон — это образец, по которому компилятор автоматически генерирует конкретный код под нужные типы. Один код — работает с int, string, твоим классом.

Чем шаблон отличается от макроса

Шаблонные параметры — что бывает

пример из лекции
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 <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).

Затрудняют ли шаблоны разработку?

  1. Ошибки многословные и диагностируются не там, где сделаны, а в шаблоне.
  2. Сложно писать разные реализации для разных категорий типов (приходится использовать SFINAE, концепты).

Как работают шаблоны при компиляции и линковке

Шаблон — это рецепт, а не код. Реальный код появляется на этапе компиляции, когда шаблон инстанцируется (подставляются конкретные типы).

  1. Компиляция .cpp: при каждом vector<int>, vector<string> компилятор генерирует отдельный код этого класса.
  2. Несколько .cpp видят один шаблон → дубликаты: если в a.cpp и b.cpp есть vector<int>, оба сгенерируют этот класс. На этапе линковки линкер должен слить их в один (через специальный механизм — weak symbols или comdat sections).
  3. Поэтому шаблоны живут в .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,
            умру'   но движимое)
glvalue — имеет идентичность rvalue — движимое (movable)

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).
lvalue — выражение, имеющее идентифицируемое место в памяти, обычно слева от =: имена переменных, элементы массива, разыменованный указатель. rvalue — временные значения без долговременной идентичности: литералы, результаты арифметики, возвраты функций по значению. T&& — rvalue-ссылка, принимающая только rvalue. Нужна, чтобы отличить временный объект, у которого можно «украсть» содержимое, от постоянного — это основа move-семантики и оптимизации копирований.

2.3 Правило 5 · правило 3 · правило 0

Что это и что объединяет

Это специальные функции-члены класса. Если ты определяешь одну из них вручную — обычно нужно определить ВСЕ, иначе компилятор сгенерирует остальные неправильно (для класса, владеющего ресурсами).

ПравилоЧто входит
Правило 3 (C++98)деструктор + копи-конструктор + копи-оператор
Правило 5 (C++11)+ move-конструктор + move-оператор присваивания
Правило 0 (modern)не пиши ничего из этого — используй классы, которые уже всё умеют (string, vector, unique_ptr)

Где что лучше

Почему у нас правило 5, а не 3

Что объединяет правило 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=
Правило пяти — рекомендация о том, что если класс владеет ресурсами и тебе нужен один из пяти специальных методов (деструктор, копи-ctor, копи=, move-ctor, move=) — нужно реализовать все. Иначе компилятор сгенерирует поведение по умолчанию для остальных, которое будет некорректным: например, два объекта будут указывать на одну память, и при разрушении один разрушит память, другой будет с висячим указателем (double-free).

Связанные именованные требования

Это те самые имена, которые проверяет твой раздел TEST(NAMED_REQS, ...) в тестах.

2.4 this и как его скопировать без лямбды

Что такое this

this — это указатель на текущий объект внутри метода класса. В нестатических методах он всегда неявно есть. Через него ты обращаешься к полям и методам.

class A {
    int x;
    void show() {
        std::cout << this->x;   // явно
        std::cout << x;         // эквивалентно
    }
};

Тип this

Поэтому если ты в 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 — это указатель на текущий объект внутри его метода, неявно передающийся при каждом вызове нестатического метода. Чтобы скопировать сам объект — пишут 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 (двунаправленный)+ --itstd::list, std::map, std::set, твой MapTree
Random Access (произвольный)+ it+5, it[i], it1 < it2vector, 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 }

Лямбда == класс

// Лямбда:
[]() { std::cout << "Hello" << std::endl; }

// Компилятор разворачивает в:
class __Lambda1234 {
public:
    auto operator()() const {
        std::cout << "Hello" << std::endl;
    }
};

У лямбды:

Способы захвата

int i = 10;

[] {};            // без захвата
[i] {};           // i по копии
[&i] {};          // i по ссылке
[=] {};           // всё что использую — по копии
[&] {};           // всё — по ссылке
[=, &i] {};       // всё по копии, кроме i — по ссылке
[this] {};        // захватить указатель на объект
[*this] {};       // (C++17) захватить КОПИЮ объекта

Где можно использовать

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>:

Что такое предикат

Это функтор, возвращающий bool. Используется как «условие» в алгоритмах. std::less — бинарный предикат «меньше».

Что сравнивает std::less

Два значения через <. По умолчанию используется ассоциативными контейнерами для упорядочения ключей:

template <class T>
struct less {
    bool operator()(const T& a, const T& b) const {
        return a < b;
    }
};

Особенности std::less

Пример своего компаратора

// Сравнение строк без учёта регистра
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;
std::less — это стандартный функтор-предикат, реализующий сравнение «меньше» через 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 ещё используется для импорта имён из 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;
// ...
using — это синтаксис для создания псевдонима типа, проброса методов базы при private-наследовании, или импорта имени из namespace. typedef — старая форма того же, ограниченная: не работает с шаблонами. typename — ключевое слово, нужное внутри шаблонов, чтобы сказать компилятору «это тип» в случаях, когда без него компилятор не может догадаться сам (например, 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.

Red-Black tree (красно-чёрное дерево)

Балансируется через цвета (Байер 1972, Гибас-Седжвик 1978).

5 правил RB:

  1. Каждый узел красный или чёрный.
  2. Корень всегда чёрный.
  3. NIL-листы (nullptr) считаются чёрными.
  4. У красного узла НЕТ красных детей (нет двух красных подряд).
  5. От любого узла до его NIL-потомков на всех путях — одинаковое число чёрных узлов.
          50
         /    \
     30      70
     /  \     /  \
   20 40 60 80
красный чёрный

Сравнение

AVLRB
Хранитint heightbool red
Высота≤ 1.44·log n≤ 2·log n
Поискчуть быстреечуть медленнее
Вставка/удалениедорожедешевле
Стиль кода у тебярекурсивныйитеративный
BST — двоичное дерево поиска, в котором слева от узла лежат меньшие ключи, справа — большие. Гарантирует O(log n) поиск при условии сбалансированности. AVL — самобалансирующееся BST через жёсткое ограничение разницы высот ≤ 1, хранит высоту узла. Red-Black — самобалансирующееся BST через 5 цветовых правил, хранит цвет узла. RB используется в std::map, потому что баланс «работы и качества» лучше для смешанных операций.

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]

Где в твоём коде

Дополнительный сценарий — 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 — пусть компилятор сам генерит.

Все пять методов отвечают за жизненный цикл и владение ресурсами:

  1. Деструктор — освобождает ресурсы.
  2. Копи-ctor — создаёт независимую копию.
  3. Копи= — заменяет содержимое копией.
  4. Move-ctor — крадёт ресурсы из временного.
  5. 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 раз — глупо. Шаблон позволяет написать ОДИН раз, и компилятор сгенерирует версию под каждый тип.

Что это: рецепт кода. Сам шаблон НЕ компилируется в машинный код. Машинный код появляется только когда ты используешь шаблон с конкретными типами.

Виды параметров шаблона:

  1. Тип: template<typename T> или template<class T> (одинаково).
  2. Константа компиляции (non-type): template<int N>, template<bool Flag>.
  3. Шаблон шаблона: template<template<typename> class Container>.
  4. 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 — аргументы как у обычной функции.
  • specifiersmutable, constexpr, ...
  • exception-specificationnoexcept.
  • 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).
  • 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 — два смысла:

  1. В параметрах шаблона: template<typename T> — то же что template<class T>.
  2. Внутри шаблонов перед именем, зависящим от параметра, — чтобы сказать «это тип»:
    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> — компилятор:

  1. Проверяет, не инстанцировал ли он уже этот класс с такими параметрами в текущем .cpp.
  2. Если нет — генерирует ВЕСЬ код этого класса прямо в текущий объектный файл, помечая символы как 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 категорий:

  1. Input — только чтение, только вперёд, один проход. Пример: istream_iterator (чтение из файла).
  2. Output — только запись, только вперёд, один проход. Пример: ostream_iterator (запись в файл).
  3. Forward — чтение + запись, только вперёд, многократный проход. Пример: std::forward_list.
  4. Bidirectional — + --it. Пример: std::list, std::map, мой MapTree.
  5. 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). В контексте ассоциативных контейнеров — задаёт порядок ключей.

Почему нужен:

  1. Не для всех типов есть осмысленный естественный порядок.
  2. Иногда нужно нестандартное упорядочение (по убыванию, без учёта регистра, по части ключа).
  3. Пользователь должен иметь возможность задать своё правило, не меняя контейнер.

В моей лабе 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 применяют там, где много поисков и мало изменений — например, индексы в базах данных.

Гарантии корректности кода при возникновении исключений. Четыре уровня:

  1. No-throw — функция гарантированно не бросит. Помечается noexcept. Пример: swap, деструкторы, size().
  2. Strong (сильная) — либо операция выполнена полностью, либо состояние объекта НЕ ИЗМЕНИЛОСЬ. Пример: insert в std::map.
  3. Basic (базовая) — объект остаётся в валидном состоянии, инварианты сохранены, но содержимое может измениться.
  4. 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 внутри узлов.

tree_base.h · структура узла
// БАЗА — только связи
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;
};
tree_base.h · header — фейковый узел-страж
class OrderedTreeBase {
  protected:
    NodeBase header_{};   // фейковый узел:
                          //   header_.parent = корень дерева
                          //   header_.left   = минимум
                          //   header_.right  = максимум
                          //   header_.is_header = true
    size_type size_ = 0;
    Compare compare_{};
};
RBTree.h:294 · создание узла в куче
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;
}
RBTree.h:249 · удаление узла
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-каст

tree_base.h:262
Derived& self() noexcept {
    return *static_cast<Derived*>(this);
}
// База кастит this до Derived* и вызывает методы наследника.
// Это даёт статический полиморфизм без virtual.

Защита от self-assignment

tree_base.h:401
OrderedTreeBase& operator=(const OrderedTreeBase& o) {
    if (this != &o) {            // мы не сами себе?
        Derived tmp(static_cast<const Derived&>(o));
        self().swap(tmp);
    }
    return *this;
}

Strong exception safety в копи-конструкторе

tree_base.h:373
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

tree_base.h:122
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

RBTree.h:30
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-правил после вставки

RBTree.h:87 · цикл в link_and_fix
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 — рекурсивная балансировка

AVLTree.h:63
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). Факториал считается во время компиляции.

factorial.cpp · вариант 1: через специализацию
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;
}
factorial.cpp · вариант 2: через struct и constexpr
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 на этапе компиляции
factorial.cpp · вариант 3: через constexpr-функцию (C++14+)
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
На защите: я бы показывал 1-й или 2-й вариант — это «настоящее» шаблонное метапрограммирование, выглядит солиднее. Третий, через constexpr-функцию, проще и считается современным, но впечатляет меньше.

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;
}

Чек-лист перед защитой