Занятие 24. Шаблоны и исключения
Последняя языковая лекция семестра — легализация двух механизмов, которыми мы пользовались с первого дня: буква T в угловых скобках (vector<int>, sort, variant<Ts...>) и исключения, которые уже ловили (bad_variant_access, at()). Сегодня оба крупным планом: как компилятор пишет код за нас — и что происходит, когда ошибка летит через стек. Все выводы и замеры реальные (Apple M-серия, clang++ -O2).
Функции-шаблоны: штамп и штамповщик
Шаблон сам по себе — не код, а рецепт. Код появляется при инстанцировании: по одной настоящей функции на каждый использованный T — nm (занятия 1 и 11) показывает обе. T выводится из аргументов по правилам, родственным auto (занятие 11). Отсюда же ответ на вопрос «почему шаблоны живут в заголовках»: штамповщику нужен рецепт целиком в каждой единице трансляции, а дубликаты потом склеит линкер.
Тонкости вывода:
Каждая инстанциация — отдельная функция со своей static (занятие 11, смысл № 1 — теперь помноженный на число типов). Цена штамповки: каждый T добавляет код в бинарник — «bloat» реален.
Классы-шаблоны: DynArray дорастает до DynArray<T>
Класс из занятий 7 → 16 → 22 делает финальный шаг: тип элемента становится параметром — это и есть vector, мы честно дошли до него сами. Методы инстанцируются лениво: не вызвали — не отштамповано (потому vector<T> компилируется и для T без operator< — пока не позовёте sort). Весь STL занятий 16–17 — одна большая штамповочная фабрика.
Цена дженериков: ошибка из глубины
Шаблон проверяется в точке подстановки: sort честно штампуется для Point и падает глубоко внутри, где впервые встретилось a < b. Итог — 308 строк про внутренности libc++, и ни одна не указывает на вашу строку.
Концепты C++20: 308 строк против 19
Концепт — именованное требование к типу; ставится на место typename и проверяется до штамповки. Ошибка сжалась в 16 раз, указывает на вызов и называет нарушенный контракт по имени. Стандартная библиотека концептов готова: totally_ordered, integral, copyable, invocable…
Свой контракт — через requires-выражение:
Читается как интерфейс, но проверяется статически — без vtable и наследования. К дихотомии занятия 20 (закрытый variant против открытого наследования) концепты добавляют третий путь: открытый набор типов со статической проверкой. Это продолжение главной линии курса «расскажи компилятору больше»: enum class (11) → variant (20) → концепты.
Правило курса
Публичная шаблонная функция — с концептом; «голый typename» оставляем внутренним помощникам.
Исключения: ошибки, которые летают
throw прерывает функцию и летит вверх по стеку вызовов (занятие 5), пока не встретит подходящий catch. Ловим по const& — без копий и срезки; порядок catch-блоков — от частного к общему. Стандартная иерархия: exception → logic_error/runtime_error → знакомые bad_variant_access, out_of_range.
Раскрутка стека: RAII сдаёт главный экзамен
Пока исключение летит, каждый пролетаемый кадр стека аккуратно убирается: локальные объекты умирают в обратном порядке, деструкторы вызваны, ресурсы освобождены. Это финальный экзамен RAII: занятие 11 показало утечку на раннем return — исключения делают ручной delete окончательно безнадёжным, а RAII-владельцев (vector, string, unique_ptr из занятия 14) — обязательными. Сырой указатель в пролетаемом кадре — гарантированная утечка; RAII-объект — гарантированная уборка.
Цена: бесплатно, пока не летит
Современная модель — «zero-cost»: пока исключение не брошено, накладных почти нет. Но сам полёт дорог: ~2.3 микросекунды на бросок (таблицы раскрутки, поиск обработчика) — в тысячи раз дороже возврата кода.
Правило курса
Исключения — для исключительного: сломанный инвариант, невозможность продолжать. Ожидаемые ошибки данных — Result-variant из занятия 20. И замыкание занятия 22: исключение, вылетевшее из noexcept-функции, — мгновенный std::terminate; потому move-операции и обещают не бросать.
Проверьте себя
-
Что напечатает?
-
Напишите концепт Printable (тип умеет
os << x) и добейтесь короткой ошибки. -
Порядок деструкторов.
f()создаёт Guard a, Guard b, бросает, дальше Guard c. Что напечатается? А если throw случится в конструкторе b?
Домашнее задание (сдача через Git)
- DynArray<T>: обобщите свой класс из занятий 7/22 шаблоном; проверьте на int, std::string и BigInt — включая правило пяти с move.
- Концепт-апгрейд: добавьте концепты трём своим шаблонным функциям; приложите тексты ошибок до/после — с числом строк.
- Раскрутка своими глазами: Guard-класс + три уровня вложенности; письменный прогноз вывода, затем запуск; вариант с исключением из конструктора.
- Замер цены: воспроизведите таблицу happy/error path; найдите долю ошибок, при которой обе версии сравниваются по времени.
- * Концепт Sortable (итераторы + сравнимость) и таблица «кто проходит» для пяти контейнеров занятия 17.
Следующее занятие — двоичная куча: добавление и удаление минимума, построение за O(n), heapsort (та самая страховка introsort из занятия 21) и очередь с приоритетами. Финальный семинар перед РК2.