Подготовка Middle C++

Вопросы на собеседовании Middle C++-разработчика

15 выбранных вопросов для Middle C++-разработчиков, которым нужно обсуждать производительность, владение ресурсами и инженерные компромиссы.

Начать ИИ-собеседование Middle C++Банковская карта не нужна. Доступна 1 бесплатная сессия.
Практика технического интервью на английскомРежим, в котором не носители языка могут потренироваться проходить интервью.

Управление памятью

1Как работают пользовательские deleter в smart pointer и когда они полезны для взаимодействия C/C++?

Пользовательский deleter — это вызываемая логика очистки, которую smart pointer использует вместо операции delete по умолчанию, когда указатель освобождает принадлежащий ему ресурс. Он полезен для взаимодействия C/C++, когда ресурс нужно освобождать конкретной функцией, например fclose, close, curl_easy_очистка, SSL_free, free или destroy-функцией библиотеки. Для unique_ptr тип deleter входит в тип unique_ptr и может влиять на его размер; stateless deleter могут быть оптимизированы, а deleter в виде указателя на функцию или stateful deleter добавляют хранилище. Для shared_ptr deleter хранится в control block и выполняется, когда последний сильный владелец освобождает объект. Пользовательские deleter позволяют C-ресурсам безопасно участвовать в RAII.

Попробовать ответить на вопрос AI-тренеру

2Объясните механики weak_ptr и control block у shared_ptr, включая циклы, enable_shared_from_this и стоимость refcount.

shared_ptr управляет совместным владением через control block, содержащий сильный и слабый счётчики ссылок, а также информацию об очистке — например, deleter и allocator. Копирование или уничтожение объектов shared_ptr увеличивает или уменьшает сильный счётчик, обычно атомарными операциями, поэтому отдельные объекты shared_ptr можно безопасно использовать из разных потоков, но каждое обновление refcount имеет стоимость. Когда сильный счётчик достигает нуля, управляемый объект уничтожается; control block остаётся, пока не исчезнут и слабые ссылки. weak_ptr указывает на тот же control block, не продлевая время жизни объекта; lock() возвращает shared_ptr, если объект ещё жив, и пустой shared_ptr в противном случае. Циклы только из shared_ptr приводят к утечкам, потому что сильные счётчики никогда не достигают нуля, поэтому weak_ptr используют для обратных указателей или observer-связей. enable_shared_from_this позволяет объекту, которым уже владеет shared_ptr, создать новый shared_ptr на себя, используя существующий control block, избегая опасных отдельных control block. Стоимость refcount включает атомарные increment/decrement, contention кэша, аллокацию control block и накладные расходы в горячих путях.

Попробовать ответить на вопрос AI-тренеру

Шаблоны

3Объясните категории значений (value categories) в C++ и perfect forwarding, а также почему это важно для эффективных обобщённых API.

Категории значений в C++ описывают выражения: lvalue имеют идентичность и к ним можно обращаться после выражения; prvalue — чистые rvalue, например многие временные/вычисленные значения; xvalue — истекающие объекты, чьи ресурсы можно повторно использовать. Perfect forwarding — это шаблонный приём: принять forwarding reference, обычно T&&, где T выводится, и переслать через std::forward<T>(arg), чтобы сохранить категорию значения вызывающего кода: lvalue остаются lvalue, а rvalue — rvalue. Это возможно благодаря правилам свёртки ссылок (reference collapsing). Это важно для обобщённых backend API, потому что обёртки, фабрики, диспетчеры и функции в стиле emplace могут избегать лишних копий и сохранять выбор перегрузок и поведение перемещения.

Попробовать ответить на вопрос AI-тренеру

Система типов

4Сравните auto, decltype, decltype(auto) и вывод аргументов шаблона в типичном backend-коде.

auto использует вывод, похожий на шаблонный, для переменных: обычный auto обычно отбрасывает ссылки и cv-квалификаторы верхнего уровня (top-level const), если объявление об этом не просит, например auto&, const auto& или auto&&. decltype(expr) более точно исследует объявленный тип или тип выражения: не взятое в скобки id-expression даёт объявленный тип, тогда как другие lvalue-выражения дают T&, xvalue — T&&, а prvalue — T. decltype(auto) выводит тип по правилам decltype, часто для возвращаемых типов, когда нужно сохранить ссылки. Вывод аргументов шаблона похож на auto, но зависит от формы параметра — T, T&, const T& или T&& — и имеет собственные правила. Braced initializer — частый источник различий: auto x = {1,2} выводит std::initializer_list<int>, тогда как обычный параметр шаблона, как правило, не может вывести T из «голого» braced initializer, если параметр не ожидает initializer_list или другой подходящий тип.

Попробовать ответить на вопрос AI-тренеру

5Сравните std::variant с полиморфизмом на основе наследования для моделирования разнородных сообщений или событий.

std::variant — value type, который хранит ровно одну альтернативу из фиксированного, закрытого множества типов и обычно обрабатывается через std::visit или явные запросы типа. Он полезен для протокольных сообщений или событий, когда множество видов сообщений известно и нужна типобезопасная обработка без virtual dispatch и часто без кучной аллокации на объект. Полиморфизм на основе наследования использует базовый класс и виртуальные функции для диспетчеризации через общий интерфейс; он лучше, когда множество производных типов сообщений открыто, независимо расширяемо, plugin-подобно или скрыто за стабильным интерфейсом. Variant благоприятствует закрытым sum types, локальности и проверкам на этапе компиляции; наследование — расширяемости, runtime-полиморфизму и интерфейсному дизайну.

Попробовать ответить на вопрос AI-тренеру

Семантика языка

6Что такое неопределённое поведение (undefined behavior) в C++ и как оно может проявляться в производственных инцидентах backend?

Неопределённое поведение (undefined behavior, UB) — это поведение, для которого стандарт C++ не накладывает никаких требований после недопустимой операции. Программа может казаться рабочей, падать, портить данные, открывать уязвимости безопасности или быть оптимизированной до неожиданного поведения. Компиляторы предполагают, что UB не происходит, и оптимизируют исходя из этого, поэтому проблемы могут проявляться только в release-сборках или под производственной нагрузкой. Backend-инциденты могут возникать из-за висячих указателей/ссылок, use-after-free, нарушений времени жизни объектов, выхода за границы, переполнения знаковых целых, гонок данных, некорректных приведений, чтения неинициализированных данных, double free или нарушений strict aliasing. Меры смягчения включают RAII и ясный дизайн владения/времени жизни, более безопасные абстракции и проверки границ, тестирование/фаззинг, code review, статический анализ и санитайзеры вроде ASan, UBSan и TSan.

Попробовать ответить на вопрос AI-тренеру

7Различите неопределённое (undefined), неуточнённое (unspecified) и определяемое реализацией (implementation-defined) поведение на примерах, релевантных для backend.

Неопределённое поведение (undefined behavior) означает, что стандарт C++ не накладывает никаких требований; результат может включать сбои, порчу данных, уязвимости безопасности или зависящую от оптимизатора некорректную компиляцию. Примеры: выход за границы массива, use-after-free, переполнение знакового целого и гонки данных (data races). Неуточнённое поведение (unspecified behavior) означает, что стандарт допускает более одного исхода, и реализация не обязана документировать, какой из них выбран; распространённый пример — порядок вычисления многих аргументов функции, поэтому код не должен зависеть от того, что побочные эффекты вычисляются в каком-то конкретном порядке. Определяемое реализацией поведение (implementation-defined behavior) означает, что реализация должна выбрать поведение и задокументировать его; примеры — знаковость обычного `char`, размеры/диапазоны некоторых фундаментальных типов в пределах требований стандарта и часть поведения знакового сдвига вправо. В backend-коде UB — это риск корректности и безопасности, а неуточнённое и определяемое реализацией поведение — риски переносимости, которых следует избегать или изолировать в протокольной, storage- и кросс-платформенной логике.

Попробовать ответить на вопрос AI-тренеру

Обработка ошибок

8Объясните уровни exception safety и то, как noexcept влияет на move-операции, контейнеры и генерацию кода.

Уровни exception safety описывают, что остаётся истинным, если операция бросает исключение. Базовая гарантия (basic guarantee) означает, что инварианты сохранены и ресурсы не утекают, хотя состояние могло измениться. Сильная гарантия (strong guarantee) означает семантику commit-or-откат: при сбое наблюдаемое состояние не меняется. Гарантия nothrow означает, что операция не бросает. Типичные техники включают RAII, выполнение на временных объектах, copy-and-swap и упорядочивание мутаций так, чтобы commit происходил только после успешного завершения работы, которая может бросить. `noexcept` — это контракт: если функция `noexcept` бросает, вызывается `std::terminate`. Он также влияет на generic-код и контейнеры: например, при реаллокации `std::vector` может перемещать элементы, когда move-конструктор `noexcept`; иначе может копировать, часто через `std::move_if_noexcept`, чтобы сохранить гарантии исключений. `noexcept` также может помочь генерации кода или оптимизации, сокращая нужные пути распространения исключений, но только когда обещание корректно.

Попробовать ответить на вопрос AI-тренеру

9Объясните std::expected или эквивалентные outcome-типы как альтернативу исключениям для fallible-операций.

`std::expected<T, E>` представляет либо успешное значение `T`, либо явную ошибку `E`, делая сбой частью возвращаемого типа функции вместо опоры на распространение исключений. Это полезно для fallible-операций, где ошибки ожидаемы и вызывающий код должен обрабатывать их локально: парсинг, валидация, сетевые вызовы, lookup в storage и повторяемые backend-операции. Тип ошибки должен нести структурированную информацию: код ошибки, категорию, retryability, сообщение или отображение в HTTP/RPC. Типы expected/outcome компонуются проверкой и распространением ошибок, а в API в стиле C++23 могут использовать монадические операции вроде `and_then`, `transform` и `or_else`, чтобы связывать шаги без глубокой вложенности условий. По сравнению с исключениями они делают поток управления и контракты API явными и хорошо работают через границы без исключений или ABI; исключения по-прежнему могут быть уместны для редких, нелокальных или действительно исключительных сбоев в зависимости от политики проекта.

Попробовать ответить на вопрос AI-тренеру

Время жизни объектов

10Опишите правила времени жизни объектов для автоматических, динамических, временных и асинхронно используемых объектов.

Автоматические объекты живут от момента конструирования до конца своей области видимости; указатели или ссылки на них становятся висячими после выхода из этой области. Динамические объекты живут от выделения/конструирования до явного уничтожения или до уничтожения владеющим объектом; «сырые» указатели и ссылки не продлевают время жизни. Временные объекты обычно живут до конца полного выражения (full expression), с отдельными правилами продления времени жизни при привязке к ссылкам, но не во всех случаях использования. Асинхронная работа — колбэки, потоки, таймеры или корутины — может выполняться после уничтожения объекта, на который ссылаются, поэтому захваты и сохранённые ссылки требуют явного управления временем жизни, чтобы избежать висячих ссылок и use-after-free.

Попробовать ответить на вопрос AI-тренеру

11Опишите проблемы порядка статической инициализации и то, как constinit, magic statics и dependency injection их смягчают.

Проблемы порядка статической инициализации возникают потому, что динамически инициализируемые объекты с областью видимости пространства имён или static-объекты в разных единицах трансляции имеют неуточнённый относительный порядок инициализации. Один глобальный объект может использовать другой до его конструирования; порядок уничтожения также может создавать похожие проблемы при завершении программы. constinit требует, чтобы переменная со static или thread-local storage проходила static/constant initialization, иначе программа ill-formed, — тем самым устраняя зависимость от порядка динамической инициализации для этой переменной. Magic statics, или function-local statics, инициализируются при первом использовании и потокобезопасны начиная с C++11. Dependency injection избегает скрытых глобальных зависимостей, конструируя объекты в контролируемом порядке и явно передавая зависимости.

Попробовать ответить на вопрос AI-тренеру

12Что такое copy elision, NRVO и RVO, и когда на них можно полагаться?

Copy elision означает конструирование объекта напрямую в его конечном месте, вместо создания отдельного временного объекта и его копирования или перемещения. RVO обычно относится к возврату безымянного временного объекта или prvalue, например return T{}; в C++17 и новее многие такие случаи обязательны, потому что prvalue напрямую инициализирует объект результата. NRVO — это возврат именованного локального объекта, например return x; компилятору разрешено конструировать этот локальный объект прямо в return-слоте вызывающего кода, но это не гарантировано. Можно полагаться на обязательный elision prvalue в C++17 в указанных случаях, но не на то, что NRVO всегда произойдёт; избегайте std::move при возврате именованного локального значения, потому что это может помешать NRVO.

Попробовать ответить на вопрос AI-тренеру

Стандартная библиотека

13Как требования к компаратору, равенству и hasher влияют на корректность ассоциативных контейнеров?

Ассоциативные контейнеры опираются на сравнение, равенство и хеширование, чтобы определять идентичность ключей и поддерживать свои внутренние инварианты. Упорядоченные контейнеры вроде std::map требуют, чтобы компаратор задавал strict weak ordering; ключи считаются эквивалентными, когда ни один не меньше другого, а не обязательно по operator==. Неупорядоченные контейнеры требуют, чтобы равенство было отношением эквивалентности, и любые два ключа, считающиеся равными, должны давать одно и то же хеш-значение. Ключи нельзя мутировать, пока они хранятся, так, чтобы менялись их упорядоченность, равенство или хеш, потому что это может сделать lookup, erase и поведение уникальности некорректными.

Попробовать ответить на вопрос AI-тренеру

14Объясните heterogeneous lookup в упорядоченных и неупорядоченных ассоциативных контейнерах.

Heterogeneous lookup позволяет искать в ассоциативном контейнере по типу, отличному от типа ключа, избегая временного конструирования ключа. В упорядоченных контейнерах для этого нужен прозрачный (transparent) компаратор, например std::less<> или пользовательский компаратор с маркером is_transparent. В неупорядоченных контейнерах нужны и transparent hash, и transparent equality-функторы, которые согласованно обрабатывают тип ключа и тип lookup. Типичный бэкенд-пример — контейнер с ключом std::string, в котором можно искать по std::string_view или const char* без аллокации временного std::string.

Попробовать ответить на вопрос AI-тренеру

Конкурентность

15Объясните модель памяти C++: гонки данных (data races), happens-before и гарантии синхронизации.

Модель памяти C++ определяет, когда операции в разных потоках упорядочены и когда записи становятся видимыми. Гонка данных (data race) возникает, когда два потока одновременно обращаются к одной и той же ячейке памяти, хотя бы одно обращение — запись, и обращения не упорядочены отношением happens-before и иным образом не сделаны безопасными атомарными операциями; гонка данных приводит к неопределённому поведению. Happens-before — отношение порядка, благодаря которому более ранние побочные эффекты видны более поздним операциям. Операции синхронизации создают этот порядок: например, unlock мьютекса synchronizes-with с более поздним успешным lock того же мьютекса, а подходящие atomic release/acquire операции могут синхронизировать потоки. Корректные программы используют мьютексы, atomics или другую синхронизацию, чтобы установить happens-before для разделяемого состояния.

Попробовать ответить на вопрос AI-тренеру