Подготовка к C++ интервью

Вопросы для собеседования по C++

Выбранные вопросы для C++ backend-разработчиков, сгруппированные по темам и взятые из того же каталога, который используется в практике EngineerSpeak.

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

Resource Management

1Объясните RAII и то, как этот принцип формирует безопасное при исключениях управление ресурсами в C++ backend-сервисах.

RAII (Resource Acquisition Is Initialization) означает, что объект C++ владеет ресурсом и освобождает его в своём деструкторе. Поскольку локальные объекты уничтожаются автоматически при завершении времени жизни/выходе из области видимости, в том числе при раскрутке стека из-за исключения, RAII обеспечивает детерминированную очистку и делает пути обработки ошибок безопасными при исключениях. В backend-сервисах это относится не только к памяти, но и к файловым дескрипторам, сокетам, блокировкам мьютексов, дескрипторам БД, транзакциям и другим ресурсам ОС или приложения.

class Fd {
public:
    explicit Fd(int fd = -1) noexcept : fd_(fd) {}
    ~Fd() { if (fd_ != -1) ::close(fd_); }

    Fd(const Fd&) = delete;
    Fd& operator=(const Fd&) = delete;

    Fd(Fd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}
    Fd& operator=(Fd&& other) noexcept {
        if (this != &other) {
            if (fd_ != -1) ::close(fd_);
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const noexcept { return fd_; }

private:
    int fd_;
};

void handle_request() {
    Fd sock(::accept(...));
    if (sock.get() == -1) throw std::runtime_error("accept failed");
    // If any later operation throws, sock is still closed by ~Fd().
}
Попробовать ответить на вопрос AI-тренеру

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

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, потому что он эффективен и безопасен при исключениях.

struct Session {};

std::unique_ptr<Session> create_session();      // caller receives ownership
void process(Session& s);                       // borrows, does not own
void maybe_process(const Session* s);           // nullable borrow

class Dispatcher {
public:
    void add(std::shared_ptr<Session> s) {      // Dispatcher shares lifetime
        sessions_.push_back(std::move(s));
    }
private:
    std::vector<std::shared_ptr<Session>> sessions_;
};
Попробовать ответить на вопрос AI-тренеру

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.

using FilePtr = std::unique_ptr<FILE, int(*)(FILE*)>;

FilePtr open_file(const char* path) {
    return FilePtr(std::fopen(path, "r"), &std::fclose);
}

struct CurlDeleter {
    void operator()(CURL* h) const noexcept {
        if (h) curl_easy_cleanup(h);
    }
};

using CurlPtr = std::unique_ptr<CURL, CurlDeleter>;
CurlPtr make_curl() {
    return CurlPtr(curl_easy_init());
}
Попробовать ответить на вопрос AI-тренеру

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 и накладные расходы в горячих путях.

struct Parent;

struct Child {
    std::weak_ptr<Parent> parent; // non-owning back-reference
};

struct Parent {
    std::vector<std::shared_ptr<Child>> children; // owns children
};

void example() {
    auto p = std::make_shared<Parent>();
    auto c = std::make_shared<Child>();
    c->parent = p;
    p->children.push_back(c);

    if (auto locked = c->parent.lock()) {
        // safe use while Parent is still alive
    }
}
Попробовать ответить на вопрос AI-тренеру

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

5Что такое move-семантика и как корректно реализовать move-конструктор и move-присваивание?

Move-семантика позволяет C++ передавать ресурсы от временных или иным образом «расходуемых» объектов вместо их копирования. Она использует rvalue-ссылки вида T&& и std::move, который является приведением типа, позволяющим выбрать move-перегрузки; сам по себе std::move ничего не перемещает. Корректный move-конструктор инициализирует новый объект, забирая ресурс у исходного и оставляя исходный валидным, уничтожаемым и присваиваемым. Корректный оператор move-присваивания переносит ресурс в уже существующий объект, обрабатывает или допускает self-assignment, освобождает или повторно использует текущий ресурс назначения, забирает ресурс источника и оставляет источник в безопасном состоянии. Move-операции часто следует помечать noexcept, чтобы стандартные контейнеры могли использовать их при реаллокации, сохраняя гарантии исключений.

class Buffer {
public:
    Buffer() = default;
    explicit Buffer(std::size_t n) : size_(n), data_(new char[n]) {}
    ~Buffer() { delete[] data_; }

    Buffer(const Buffer&) = delete;
    Buffer& operator=(const Buffer&) = delete;

    Buffer(Buffer&& other) noexcept
        : size_(std::exchange(other.size_, 0)),
          data_(std::exchange(other.data_, nullptr)) {}

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            size_ = std::exchange(other.size_, 0);
            data_ = std::exchange(other.data_, nullptr);
        }
        return *this;
    }

private:
    std::size_t size_ = 0;
    char* data_ = nullptr;
};
Попробовать ответить на вопрос AI-тренеру

6Опишите const-correctness в API на C++ и как эффективно проектировать const-функции-члены.

Const-correctness означает выражение через систему типов того, какие операции не изменяют наблюдаемое или логическое состояние объекта. У const-функции-члена объект this квалифицирован как const, поэтому она не может изменять не-mutable члены данных и вызывать не-const функции-члены того же объекта. Хороший дизайн API помечает только-для-чтения запросы как const, возвращает значения или const-ссылки/указатели, когда это уместно, и не раскрывает изменяемое внутреннее состояние из const-функций. mutable следует резервировать для деталей реализации, не меняющих логическое состояние: кэшей, ленивых значений, метрик или мьютексов. const — это контракт API о мутациях, а не автоматическая гарантия потокобезопасности; гарантии параллелизма требуют отдельной реализации и документации.

#include <mutex>
#include <optional>
#include <string>

class UserProfile {
public:
    const std::string& id() const { return id_; }

    std::string displayName() const {
        std::lock_guard<std::mutex> lock(mu_);
        if (!cached_display_name_) {
            cached_display_name_ = computeDisplayName();
        }
        return *cached_display_name_;
    }

private:
    std::string computeDisplayName() const { return id_; }

    std::string id_;
    mutable std::mutex mu_;
    mutable std::optional<std::string> cached_display_name_;
};
Попробовать ответить на вопрос AI-тренеру

7Что такое move-only типы и как они влияют на границы API для сокетов, файловых дескрипторов/хендлов и блокировок?

Move-only типы — это типы, которые нельзя копировать, но можно перемещать. Они типичны для уникального владения ресурсами вроде сокетов, файловых дескрипторов, file handles, блокировок и `std::unique_ptr`. Операции копирования удалены, чтобы избежать дублированного владения и двойного освобождения; move-операции передают владение и оставляют источник валидным, но обычно пустым/не владеющим. API должны явно выражать границы владения: фабрики могут возвращать move-only объекты по значению, функции, забирающие владение, — принимать по значению или rvalue reference, а функции только для просмотра — по ссылке, указателю или другим borrowing-хендлам. Контейнеры могут хранить move-only значения, когда элементы вставляются или перемещаются через move.

#include <utility>

class Fd {
public:
    explicit Fd(int fd = -1) noexcept : fd_(fd) {}
    ~Fd() { close_if_valid(); }

    Fd(const Fd&) = delete;
    Fd& operator=(const Fd&) = delete;

    Fd(Fd&& other) noexcept : fd_(std::exchange(other.fd_, -1)) {}

    Fd& operator=(Fd&& other) noexcept {
        if (this != &other) {
            close_if_valid();
            fd_ = std::exchange(other.fd_, -1);
        }
        return *this;
    }

    int get() const noexcept { return fd_; }
    explicit operator bool() const noexcept { return fd_ != -1; }

private:
    void close_if_valid() noexcept {
        if (fd_ != -1) {
            // ::close(fd_); // real POSIX code would close here
            fd_ = -1;
        }
    }

    int fd_;
};

Fd open_socket();               // factory returns ownership by value
void take_socket(Fd fd);        // takes ownership
void inspect_socket(const Fd&); // borrows only

void example() {
    Fd s = open_socket();
    inspect_socket(s);
    take_socket(std::move(s));
}
Попробовать ответить на вопрос AI-тренеру

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 или другой подходящий тип.

int x = 1;
const int cx = 2;
int& rx = x;

auto a = rx;        // int, copy of x
auto b = cx;        // int, top-level const dropped
const auto& c = cx; // const int&, reference preserved by declaration

a = 10;             // does not change x
Попробовать ответить на вопрос AI-тренеру

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

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, когда тип напрямую владеет ресурсом или имеет нетривиальную семантику владения/времени жизни.

struct Session {
    std::string user_id;
    std::vector<std::string> roles;
    std::unique_ptr<Connection> conn;

    // No custom destructor/copy/move operations.
};
Попробовать ответить на вопрос AI-тренеру

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

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.

#include <iostream>

struct S {
    int x;
};

int main() {
    int a;      // default-initialized: indeterminate value
    int b{};    // value/list-initialized: zero
    S s{};      // aggregate/value initialization: s.x is zero

    std::cout << b << ' ' << s.x << '\n';
    (void)a;    // reading a would be undefined behavior
}
Попробовать ответить на вопрос AI-тренеру

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.

int grow(int x) {
    if (x + 1 < x) {
        return 0; // compiler may assume this is unreachable
    }
    return x + 1;
}
Попробовать ответить на вопрос AI-тренеру

Object Model

12Объясните виртуальные функции, vtable, стоимость dynamic dispatch, object slicing и опасности виртуального деструктора.

Виртуальная функция обеспечивает runtime-полиморфизм: при вызове через указатель или ссылку на базовый тип выбирается реализация для dynamic type объекта. Большинство реализаций хранят скрытый vptr в каждом полиморфном объекте, указывающий на vtable адресов виртуальных функций для этого dynamic type. Типичная стоимость — дополнительный указатель в объекте, косвенный вызов, возможное влияние на кэш/предсказание ветвлений и меньшие возможности инлайнинга, хотя компиляторы иногда могут devirtualize. Object slicing происходит, когда derived-объект копируется или сохраняется by value как base-объект, теряя derived-часть и динамическое поведение. Если базовый класс предназначен для удаления через указатель на базу, его деструктор должен быть виртуальным; иначе удаление derived-объекта через такой base-указатель даёт неопределённое поведение.

#include <iostream>

struct Handler {
    virtual void handle() { std::cout << "base\n"; }
    virtual ~Handler() = default;
};

struct HttpHandler : Handler {
    void handle() override { std::cout << "http\n"; }
};

void run(Handler& h) {
    h.handle(); // virtual dispatch
}

int main() {
    HttpHandler h;
    run(h);
}
Попробовать ответить на вопрос AI-тренеру

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

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/хранения.

using namespace std::chrono_literals;

auto deadline = std::chrono::steady_clock::now() + 500ms;
while (!done()) {
    if (std::chrono::steady_clock::now() >= deadline) {
        throw std::runtime_error("timeout");
    }
    poll_once();
}
Попробовать ответить на вопрос AI-тренеру

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, которые откладывают форматирование или сначала проверяют уровень лога.

#include <format>
#include <string>

std::string msg = std::format("user={} latency_ms={:.2f}", user_id, latency_ms);
Попробовать ответить на вопрос AI-тренеру

Шаблоны

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 могут избегать лишних копий и сохранять выбор перегрузок и поведение перемещения.

#include <iostream>
#include <string>
#include <utility>

void sink(const std::string&) { std::cout << "const ref\n"; }
void sink(std::string&&) { std::cout << "rvalue ref\n"; }

template <class T>
void wrapper(T&& arg) {
    sink(std::forward<T>(arg));
}

int main() {
    std::string s = "hello";
    wrapper(s);
    wrapper(std::string{"x"});
}
Попробовать ответить на вопрос AI-тренеру

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

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.

struct Account {
    std::mutex m;
    int balance{};
};

void transfer(Account& from, Account& to, int amount) {
    if (&from == &to) return;

    std::scoped_lock lock(from.m, to.m);

    if (from.balance >= amount) {
        from.balance -= amount;
        to.balance += amount;
    }
}
Попробовать ответить на вопрос AI-тренеру

17Объясните типичные паттерны использования condition_variable для ожидания изменения состояния.

Используйте `std::condition_variable`, чтобы ждать изменения предиката состояния, защищённого мьютексом. Общее состояние изменяют, удерживая тот же мьютекс, затем `notify_one` или `notify_all` будит ожидающих. Ожидающие должны использовать `cv.wait(lock, predicate)` или эквивалентный цикл, потому что пробуждения могут быть ложными (spurious), а уведомления сами по себе не запоминаются отдельно от состояния предиката. `notify_one` будит одного ожидающего; `notify_all` будит всех и подходит для широковещательных изменений состояния, например shutdown.

std::mutex m;
std::condition_variable cv;
std::queue<int> q;
bool closed = false;

void push(int x) {
    {
        std::lock_guard<std::mutex> lk(m);
        q.push(x);
    }
    cv.notify_one();
}

std::optional<int> pop() {
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [&] { return !q.empty() || closed; });

    if (q.empty()) return std::nullopt; // closed and drained
    int x = q.front();
    q.pop();
    return x;
}

void close() {
    {
        std::lock_guard<std::mutex> lk(m);
        closed = true;
    }
    cv.notify_all();
}
Попробовать ответить на вопрос AI-тренеру

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, возвращаемые значения и гарантии конкурентности.

template<class T>
class BoundedQueue {
public:
    explicit BoundedQueue(std::size_t cap) : cap_(cap) {}

    // Returns false if the queue has been closed before the item is accepted.
    bool push(T value) {
        std::unique_lock<std::mutex> lk(m_);
        not_full_.wait(lk, [&] { return q_.size() < cap_ || closed_; });
        if (closed_) return false;

        q_.push_back(std::move(value));
        lk.unlock();
        not_empty_.notify_one();
        return true;
    }

    // Returns nullopt when closed and drained.
    std::optional<T> pop() {
        std::unique_lock<std::mutex> lk(m_);
        not_empty_.wait(lk, [&] { return !q_.empty() || closed_; });
        if (q_.empty()) return std::nullopt;

        T value = std::move(q_.front());
        q_.pop_front();
        lk.unlock();
        not_full_.notify_one();
        return value;
    }

    void close() {
        {
            std::lock_guard<std::mutex> lk(m_);
            closed_ = true;
        }
        not_empty_.notify_all();
        not_full_.notify_all();
    }

private:
    const std::size_t cap_;
    std::mutex m_;
    std::condition_variable not_empty_;
    std::condition_variable not_full_;
    std::deque<T> q_;
    bool closed_ = false;
};
Попробовать ответить на вопрос AI-тренеру

Database

19Как выполнить миграцию схемы для C++-сервиса без простоя?

Используйте поэтапную миграцию expand-contract. Сначала сделайте обратно совместимые дополнения схемы — например, nullable-столбцы или новые таблицы, — которые не ломают уже работающий C++-сервис. Задеплойте совместимый код, способный работать и со старым, и с новым представлением, выполните backfill существующих данных небольшими throttled-батчами, проверьте согласованность, переключите чтение на новую схему и только позже удалите старые столбцы или код, когда все задеплоенные версии больше от них не зависят. Для больших таблиц избегайте долгой блокирующей DDL, используйте online/concurrent-операции там, где они поддерживаются, мониторьте блокировки/репликацию/ресурсы и сохраняйте пути отката, работающие при частично задеплоенном коде и частично мигрированных данных.

1. Expand: add new nullable column user_display_name.
2. Deploy compatible code: write both name and user_display_name; read new if present, else old.
3. Backfill: copy name -> user_display_name in small batches.
4. Validate: compare counts/checksums/null rates and monitor errors.
5. Switch: read only user_display_name after confidence.
6. Contract: stop writing old name, then drop old column in a later release.
Попробовать ответить на вопрос AI-тренеру

20Как C++-сервису следует читать из реплик базы данных, управляя устаревшими чтениями и гарантиями read-your-writes?

Относитесь к чтениям с реплик как к явному компромиссу по согласованности. Чтения, которые допускают устаревание, могут идти на здоровые реплики, но read-your-writes, транзакционные или критичные к свежести чтения должны идти на primary, если только выбранная реплика заведомо воспроизвела релевантную позицию записи — например LSN, timestamp или version. Отслеживайте лаг и здоровье реплик, кодируйте требуемую согласованность на уровне endpoint'а или запроса и делайте резервный переход на primary, ждите догона или завершайтесь ошибкой, когда реплики превышают допустимое устаревание. Документируйте модель согласованности, чтобы вызывающие стороны знали, какие чтения могут быть устаревшими.

ReadPolicy EVENTUAL(max_staleness=2s):
  choose healthy replica with lag <= 2s; else primary or error depending on SLA

ReadPolicy READ_YOUR_WRITES(session_lsn):
  choose replica only if replay_lsn >= session_lsn;
  otherwise wait briefly or route to primary

ReadPolicy STRONG:
  route to primary
Попробовать ответить на вопрос AI-тренеру