1Объясните RAII и то, как этот принцип формирует безопасное при исключениях управление ресурсами в C++ backend-сервисах.
RAII (Resource Acquisition Is Initialization) означает, что объект C++ владеет ресурсом и освобождает его в своём деструкторе. Поскольку локальные объекты уничтожаются автоматически при завершении времени жизни/выходе из области видимости, в том числе при раскрутке стека из-за исключения, RAII обеспечивает детерминированную очистку и делает пути обработки ошибок безопасными при исключениях. В backend-сервисах это относится не только к памяти, но и к файловым дескрипторам, сокетам, блокировкам мьютексов, дескрипторам БД, транзакциям и другим ресурсам ОС или приложения.
2Сравните std::unique_ptr и std::shared_ptr и опишите, когда каждый из них уместен в backend API.
std::unique_ptr выражает исключительное владение: он дёшев, перемещаем, но не копируем, и подходит для единственного владельца или для API, которые передают владение. std::shared_ptr выражает совместное владение: он копируем и удерживает объект живым через подсчёт ссылок, пока последний сильный владелец его не отпустит. В backend API используйте unique_ptr, когда владение передаётся, shared_ptr — только когда несколько независимых владельцев должны продлевать время жизни, а ссылки или «сырые» указатели — для доступа без владения. std::make_shared обычно предпочтителен при создании объектов shared_ptr, потому что он эффективен и безопасен при исключениях.
3Как работают пользовательские 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.
4Объясните механики 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 и накладные расходы в горячих путях.
5Что такое move-семантика и как корректно реализовать move-конструктор и move-присваивание?
Move-семантика позволяет C++ передавать ресурсы от временных или иным образом «расходуемых» объектов вместо их копирования. Она использует rvalue-ссылки вида T&& и std::move, который является приведением типа, позволяющим выбрать move-перегрузки; сам по себе std::move ничего не перемещает. Корректный move-конструктор инициализирует новый объект, забирая ресурс у исходного и оставляя исходный валидным, уничтожаемым и присваиваемым. Корректный оператор move-присваивания переносит ресурс в уже существующий объект, обрабатывает или допускает self-assignment, освобождает или повторно использует текущий ресурс назначения, забирает ресурс источника и оставляет источник в безопасном состоянии. Move-операции часто следует помечать noexcept, чтобы стандартные контейнеры могли использовать их при реаллокации, сохраняя гарантии исключений.
6Опишите const-correctness в API на C++ и как эффективно проектировать const-функции-члены.
Const-correctness означает выражение через систему типов того, какие операции не изменяют наблюдаемое или логическое состояние объекта. У const-функции-члена объект this квалифицирован как const, поэтому она не может изменять не-mutable члены данных и вызывать не-const функции-члены того же объекта. Хороший дизайн API помечает только-для-чтения запросы как const, возвращает значения или const-ссылки/указатели, когда это уместно, и не раскрывает изменяемое внутреннее состояние из const-функций. mutable следует резервировать для деталей реализации, не меняющих логическое состояние: кэшей, ленивых значений, метрик или мьютексов. const — это контракт API о мутациях, а не автоматическая гарантия потокобезопасности; гарантии параллелизма требуют отдельной реализации и документации.
7Что такое move-only типы и как они влияют на границы API для сокетов, файловых дескрипторов/хендлов и блокировок?
Move-only типы — это типы, которые нельзя копировать, но можно перемещать. Они типичны для уникального владения ресурсами вроде сокетов, файловых дескрипторов, file handles, блокировок и `std::unique_ptr`. Операции копирования удалены, чтобы избежать дублированного владения и двойного освобождения; move-операции передают владение и оставляют источник валидным, но обычно пустым/не владеющим. API должны явно выражать границы владения: фабрики могут возвращать move-only объекты по значению, функции, забирающие владение, — принимать по значению или rvalue reference, а функции только для просмотра — по ссылке, указателю или другим borrowing-хендлам. Контейнеры могут хранить move-only значения, когда элементы вставляются или перемещаются через move.
8Сравните 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 или другой подходящий тип.
9Опишите Rule of Zero, Rule of Three и Rule of Five и когда применяется каждое из них.
Rule of Zero: предпочитайте классы, которые не объявляют пользовательские деструктор/операции копирования/перемещения; пусть ресурсами управляют RAII-члены вроде std::string, std::vector, std::unique_ptr, обёрток файлов/сокетов и т.п. Rule of Three: если класс вручную управляет ресурсом и ему нужен пользовательский деструктор, конструктор копирования или оператор присваивания копированием, обычно нужны все три, чтобы корректно определить поведение копирования/владения. Rule of Five: в C++11 и новее такие типы также должны учитывать конструктор перемещения и оператор присваивания перемещением. Используйте Rule of Zero для большинства прикладных типов; используйте Rule of Three/Five, когда тип напрямую владеет ресурсом или имеет нетривиальную семантику владения/времени жизни.
10Опишите формы инициализации в C++ и типичные ловушки, включая initializer_list и aggregates.
В C++ есть несколько форм инициализации. Default initialization, например `T x;`, вызывает конструктор по умолчанию для классовых типов, но оставляет автоматические fundamental-переменные неинициализированными. Value initialization, например `T x{};` или `T()`, при необходимости zero-инициализирует до инициализации конструктором/членами. List initialization использует фигурные скобки, отклоняет сужающие преобразования и имеет особые правила разрешения перегрузок, включая сильное предпочтение жизнеспособных конструкторов `std::initializer_list`. Aggregate initialization инициализирует члены aggregate напрямую фигурными скобками; C++20 также поддерживает designated initializers для aggregates в порядке объявления. Типичные ловушки: неинициализированные локальные скаляры, неожиданный выбор перегрузки `initializer_list`, ошибки сужения с фигурными скобками, most-vexing parse со скобками и изменение поведения, когда тип перестаёт быть aggregate.
11Что такое неопределённое поведение (undefined behavior) в C++ и как оно может проявляться в производственных инцидентах backend?
Неопределённое поведение (undefined behavior, UB) — это поведение, для которого стандарт C++ не накладывает никаких требований после недопустимой операции. Программа может казаться рабочей, падать, портить данные, открывать уязвимости безопасности или быть оптимизированной до неожиданного поведения. Компиляторы предполагают, что UB не происходит, и оптимизируют исходя из этого, поэтому проблемы могут проявляться только в release-сборках или под производственной нагрузкой. Backend-инциденты могут возникать из-за висячих указателей/ссылок, use-after-free, нарушений времени жизни объектов, выхода за границы, переполнения знаковых целых, гонок данных, некорректных приведений, чтения неинициализированных данных, double free или нарушений strict aliasing. Меры смягчения включают RAII и ясный дизайн владения/времени жизни, более безопасные абстракции и проверки границ, тестирование/фаззинг, code review, статический анализ и санитайзеры вроде ASan, UBSan и TSan.
12Объясните виртуальные функции, vtable, стоимость dynamic dispatch, object slicing и опасности виртуального деструктора.
Виртуальная функция обеспечивает runtime-полиморфизм: при вызове через указатель или ссылку на базовый тип выбирается реализация для dynamic type объекта. Большинство реализаций хранят скрытый vptr в каждом полиморфном объекте, указывающий на vtable адресов виртуальных функций для этого dynamic type. Типичная стоимость — дополнительный указатель в объекте, косвенный вызов, возможное влияние на кэш/предсказание ветвлений и меньшие возможности инлайнинга, хотя компиляторы иногда могут devirtualize. Object slicing происходит, когда derived-объект копируется или сохраняется by value как base-объект, теряя derived-часть и динамическое поведение. Если базовый класс предназначен для удаления через указатель на базу, его деструктор должен быть виртуальным; иначе удаление derived-объекта через такой base-указатель даёт неопределённое поведение.
13Опишите часы, точки времени и длительности в std::chrono для таймаутов сервисов, метрик и временных меток.
`std::chrono` моделирует время с помощью часов (clocks), `time_point` и `duration`. `duration` — это интервал с единицей измерения, например миллисекунды или секунды. `time_point` — это точка на шкале времени конкретных часов. `steady_clock` монотонны и должны использоваться для измерения прошедшего времени, таймаутов сервисов, дедлайнов и задержек (latency), потому что на них не влияют изменения системных (wall-clock) часов. `system_clock` представляют гражданское/настенное время и подходят для временных меток, логирования, персистентности и календарных преобразований, но могут «прыгать», когда системное время корректируют. Таймауты обычно следует строить как `steady_clock::now() + duration`; машинные временные метки должны использовать явно задокументированный формат настенного времени, как правило UTC на границах API/хранения.
14Опишите std::format и современные средства форматирования в сравнении с iostreams и printf-style API.
`std::format` — это type-safe средство форматирования C++20, вдохновлённое fmtlib. Оно использует поля подстановки `{}` и спецификации формата для получения отформатированного текста без stateful-синтаксиса вставки iostreams и без C varargs в стиле `printf`. По сравнению с `printf` оно избегает многих проблем несоответствия формата и типа; по сравнению с iostreams часто понятнее и проще для композиции. fmtlib — широко используемая библиотека, которая предшествовала `std::format` и повлияла на него, и может давать более широкую поддержку или более новые возможности. Пользовательские типы можно форматировать через поддержку custom formatter. В high-volume логировании производительность зависит от избежания ненужного форматирования, преобразований и аллокаций, особенно для отключённых уровней логов; предпочтительны API, которые откладывают форматирование или сначала проверяют уровень лога.
15Объясните категории значений (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 могут избегать лишних копий и сохранять выбор перегрузок и поведение перемещения.
16Опишите предотвращение взаимных блокировок (deadlock) и стратегии захвата нескольких мьютексов с помощью std::scoped_lock и std::lock.
Deadlock предотвращается избеганием циклического ожидания: захватывайте блокировки в согласованном глобальном порядке, когда это возможно, либо захватывайте несколько мьютексов через `std::lock`/`std::scoped_lock`, которые используют алгоритм избегания deadlock. `std::scoped_lock lock(a, b, ...)` — простейшая RAII-форма для захвата нескольких мьютексов с автоматическим освобождением при выходе из области видимости. С `std::lock` сначала захватите мьютексы, затем прикрепите RAII-обёртки с `std::adopt_lock`, либо используйте `std::unique_lock` с `std::defer_lock`. Держите критические секции короткими и избегайте блокирующих операций или неизвестных callback'ов, удерживая блокировки; избегание deadlock само по себе не гарантирует справедливость и не предотвращает все сценарии livelock/starvation.
17Объясните типичные паттерны использования condition_variable для ожидания изменения состояния.
Используйте `std::condition_variable`, чтобы ждать изменения предиката состояния, защищённого мьютексом. Общее состояние изменяют, удерживая тот же мьютекс, затем `notify_one` или `notify_all` будит ожидающих. Ожидающие должны использовать `cv.wait(lock, predicate)` или эквивалентный цикл, потому что пробуждения могут быть ложными (spurious), а уведомления сами по себе не запоминаются отдельно от состояния предиката. `notify_one` будит одного ожидающего; `notify_all` будит всех и подходит для широковещательных изменений состояния, например shutdown.
18Спроектируйте потокобезопасную ограниченную очередь (bounded queue) на стандартных примитивах C++ и определите гарантии её API.
Ограниченную потокобезопасную очередь можно построить на `std::mutex`, condition variables, контейнере фиксированной ёмкости и флаге shutdown/closed. `push` блокируется, истекает по timeout или завершается с ошибкой, когда очередь полна — это обеспечивает backpressure; `pop` блокируется, когда очередь пуста. Обе операции должны ждать по предикатам вроде `size < capacity || closed` и `!empty || closed`. При shutdown/close нужно разбудить заблокированных производителей и потребителей; отклонять новые push и определить, дренируют ли потребители уже имеющиеся элементы или останавливаются сразу. API должен задавать блокирующее поведение, семантику close, возвращаемые значения и гарантии конкурентности.
19Как выполнить миграцию схемы для C++-сервиса без простоя?
Используйте поэтапную миграцию expand-contract. Сначала сделайте обратно совместимые дополнения схемы — например, nullable-столбцы или новые таблицы, — которые не ломают уже работающий C++-сервис. Задеплойте совместимый код, способный работать и со старым, и с новым представлением, выполните backfill существующих данных небольшими throttled-батчами, проверьте согласованность, переключите чтение на новую схему и только позже удалите старые столбцы или код, когда все задеплоенные версии больше от них не зависят. Для больших таблиц избегайте долгой блокирующей DDL, используйте online/concurrent-операции там, где они поддерживаются, мониторьте блокировки/репликацию/ресурсы и сохраняйте пути отката, работающие при частично задеплоенном коде и частично мигрированных данных.
20Как C++-сервису следует читать из реплик базы данных, управляя устаревшими чтениями и гарантиями read-your-writes?
Относитесь к чтениям с реплик как к явному компромиссу по согласованности. Чтения, которые допускают устаревание, могут идти на здоровые реплики, но read-your-writes, транзакционные или критичные к свежести чтения должны идти на primary, если только выбранная реплика заведомо воспроизвела релевантную позицию записи — например LSN, timestamp или version. Отслеживайте лаг и здоровье реплик, кодируйте требуемую согласованность на уровне endpoint'а или запроса и делайте резервный переход на primary, ждите догона или завершайтесь ошибкой, когда реплики превышают допустимое устаревание. Документируйте модель согласованности, чтобы вызывающие стороны знали, какие чтения могут быть устаревшими.