Функции — единица структуры и контракт между TU
Функции в C++ — это не «именованные блоки кода», а базовая единица контракта между единицами трансляции. От того, как функция объявлена, как принимает аргументы, как возвращает результат и какие даёт гарантии (noexcept, [[nodiscard]]), зависит и интерфейс библиотеки, и поведение оптимизатора, и набор ошибок, которые можно поймать на этапе сборки.
Многие тонкие баги в C++ — висячие ссылки, неожиданные копирования, нарушения ODR, неоднозначные перегрузки — это не баги функции как таковой, а баги её сигнатуры. Поэтому стоит думать о функции как о структуре с пятью независимыми измерениями: где она объявлена и определена, как она принимает, как она возвращает, какие даёт гарантии, и как её можно передавать как значение.
C++ исторически наслаивал на функции дополнительные сущности — функторы (классы с operator()), лямбды (анонимные функции с захватом контекста), std::function (type-erased обёртка). Все они подчиняются тем же правилам сигнатуры и перегрузки, но дают разные компромиссы между гибкостью и стоимостью. Полная карта — в слоях ниже.
Карта темы
- Объявление, определение и ODR — declaration vs definition, ODR,
inline, аргументы по умолчанию, что можно класть в заголовок. - Параметры — value, ref, const&, move — четыре способа передачи, move-семантика, forwarding reference, ref-qualifiers на методах.
- Перегрузка и overload resolution — что входит в сигнатуру, ранжирование кандидатов, неоднозначность, скрытие при наследовании, SFINAE/концепты.
- Возврат значений, noexcept и стек — RVO/NRVO, structured bindings,
noexcept,[[nodiscard]], рекурсия и stack frame. - Функторы — классы с
operator(),std::function,std::bind, цена type erasure. - Лямбда-выражения — захваты, generic lambdas, mutable, constexpr-лямбды, IIFE, рекурсивные лямбды.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Определение не-inline функции в заголовке | multiple definition на этапе линковки |
return std::move(local); | Подавление NRVO — копирование/перемещение вместо элизии |
Передача большого объекта по значению вместо const& | Лишняя копия на горячем пути |
Использование T* там, где нужна непустая ссылка | Теряется гарантия «не nullptr» |
Считать T&& в шаблоне всегда rvalue-ссылкой | Это forwarding reference — связывается и с lvalue |
| Изменение дефолтного аргумента в заголовке | Тихий ABI-break — клиенты с разными дефолтами |
| Две перегрузки, отличающиеся только типом возврата | Ошибка компиляции — return type не в сигнатуре |
Скрытие наследованных перегрузок без using Base::f | d.f(1) не компилируется, хотя Base::f(int) существует |
noexcept на функции, которая может бросить | std::terminate при первом же исключении |
| Глубокая рекурсия без хвостовой оптимизации | Stack overflow в release; разный результат в debug/release |
Значение для собеседований
Функции на интервью — это тест на C++-семантику, а не на синтаксис. Кандидат, который путает объявление с определением или считает, что inline про оптимизацию, обычно не пройдёт middle-уровень.
Что обычно проверяют:
- Разница между объявлением и определением, ODR, зачем
inlineв заголовке. - Когда использовать
const&, когдаTпо значению, когдаT&&. - Что такое forwarding reference и чем отличается от rvalue reference.
- Что произойдёт с
std::vector::push_back, если move-конструктор элемента неnoexcept. - Как разрешается
f(1.0f), если естьf(int)иf(double). - Что такое RVO/NRVO и почему
return std::move(local)— антипаттерн. - Чем
std::functionотличается от указателя на функцию по производительности.
Типичный неверный ответ: «inline ускоряет функцию, заставляя компилятор подставить тело». Это запускает обсуждение того, что в современном компиляторе inline как hint практически игнорируется, а реальное назначение слова — разрешить несколько определений в разных TU.