1Як працюють користувацькі функції звільнення ресурсу (custom deleters) у розумних вказівниках і коли вони корисні для сумісності C/C++?
Користувацька функція звільнення ресурсу (custom deleter) — це викликувана логіка очищення, яку розумний вказівник використовує замість операції delete за замовчуванням, коли звільняє ресурс, яким володіє. Вона корисна для сумісності C/C++, коли ресурс потрібно звільняти конкретною функцією, наприклад fclose, close, curl_easy_cleanup, SSL_free, free або функцією знищення з бібліотеки. Для unique_ptr тип функції звільнення є частиною типу unique_ptr і може впливати на його розмір; безстанові функції звільнення можуть бути оптимізовані геть, тоді як вказівники на функції або функції зі станом додають місце для зберігання. Для shared_ptr функція звільнення зберігається в керівному блоці (control block) і виконується, коли останній сильний власник звільняє об’єкт. Користувацькі функції звільнення дають змогу ресурсам C безпечно брати участь у RAII (Resource Acquisition Is Initialization).
2Поясніть weak_ptr і механіку керівного блоку shared_ptr, зокрема цикли, enable_shared_from_this та вартість лічильників посилань.
shared_ptr керує спільним володінням через керівний блок (control block), що містить сильні та слабкі лічильники посилань (refcount), а також інформацію для очищення, зокрема функцію звільнення ресурсу (deleter) та алокатор. Копіювання або знищення об'єктів shared_ptr збільшує або зменшує сильний лічильник, зазвичай атомарними операціями, тож окремі об'єкти shared_ptr можна безпечно змінювати з різних потоків, але кожне оновлення лічильника посилань має вартість. Коли сильний лічильник досягає нуля, керований об'єкт знищується; керівний блок залишається, доки не зникнуть і слабкі посилання. weak_ptr вказує на той самий керівний блок, не подовжуючи час життя об'єкта; lock() повертає shared_ptr, якщо об'єкт ще живий, і порожній shared_ptr інакше. Цикли лише з shared_ptr спричиняють витоки, бо сильні лічильники ніколи не досягають нуля, тому weak_ptr використовують для зворотних вказівників або зв'язків спостерігача. enable_shared_from_this дозволяє об'єкту, яким уже володіє shared_ptr, створити новий shared_ptr на себе з використанням наявного керівного блоку, уникаючи небезпечних окремих керівних блоків. Вартість лічильників посилань включає атомарні збільшення/зменшення, конкуренцію за кеш, виділення керівного блоку та накладні витрати в критичних за продуктивністю ділянках коду.
3Поясніть категорії значень (value categories) та ідеальне передавання (perfect forwarding) у C++, а також чому вони важливі для ефективних узагальнених API (Application Programming Interface).
Категорії значень у C++ описують вирази: `lvalue` мають ідентифікатор і на них можна посилатися після виразу; `prvalue` — це чисті `rvalue`, такі як багато тимчасових або обчислених значень; `xvalue` — це об'єкти, час життя яких закінчується, а їхні ресурси можуть бути використані повторно. Ідеальне передавання — це шаблонна техніка приймання універсального посилання (forwarding reference), зазвичай `T&&`, де `T` виводиться (deduced), і передавання за допомогою `std::forward<T>(arg)`, щоб зберегти категорію значення викликача: `lvalue` залишаються `lvalue`, а `rvalue` залишаються `rvalue`. Правила згортання посилань (reference collapsing) роблять це можливим. Це важливо для узагальнених бекенд-API, оскільки обгортки, фабрики, диспетчери та функції стилю `emplace` можуть уникати непотрібних копіювань і зберігати вибір перевантаження та поведінку переміщення.
4Порівняйте `auto`, `decltype`, `decltype(auto)` та виведення шаблонних аргументів (template argument deduction) у типовому бекенд-коді.
`auto` використовує подібне до шаблонів виведення типів для змінних: звичайний `auto` зазвичай відкидає посилання та специфікатор `const` верхнього рівня (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 initializers): `auto x = {1,2}` виводить `std::initializer_list<int>`, тоді як звичайний шаблонний параметр зазвичай не може вивести `T` з простого ініціалізатора у фігурних дужках, якщо параметр не очікує на `initializer_list` або інший відповідний тип.
5Порівняйте `std::variant` з поліморфізмом на основі успадкування для моделювання гетерогенних повідомлень або подій.
`std::variant` — це тип-значення, який зберігає рівно одну альтернативу з фіксованого, закритого набору типів і зазвичай обробляється за допомогою `std::visit` або явних запитів типу. Він корисний для повідомлень протоколу або подій, коли набір видів повідомлень відомий і вам потрібна безпечна за типами обробка без віртуальної диспетчеризації (virtual dispatch) та часто без виділення пам'яті в купі для кожного об'єкта. Поліморфізм на основі успадкування використовує базовий клас і віртуальні функції для диспетчеризації через спільний інтерфейс; він кращий, коли набір похідних типів повідомлень є відкритим, незалежно розширюваним, схожим на плагіни або прихованим за стабільним інтерфейсом. Варіант віддає перевагу закритим типам-сумам, локальності та перевірці під час компіляції; успадкування віддає перевагу розширюваності, поліморфізму під час виконання та дизайну на основі інтерфейсів.
6Що таке невизначена поведінка (undefined behavior) у C++, і як вона може проявлятися в інцидентах на робочих серверах (production) бекенду?
Невизначена поведінка (UB - Undefined Behavior) — це поведінка, щодо якої стандарт C++ не встановлює жодних вимог після виконання неприпустимої операції. Може здаватися, що програма працює, або ж вона може завершитися аварійно, пошкодити дані, створити вразливості в безпеці або бути оптимізованою до несподіваної поведінки. Компілятори припускають, що UB не трапляється, і виконують оптимізацію на основі цього припущення, тому проблеми можуть з'являтися лише в релізних збірках (release builds) або під робочим (production) навантаженням. Інциденти в бекенді можуть виникати через висячі вказівники або посилання (dangling pointers/references), використання пам'яті після її звільнення (use-after-free), порушення часу життя об'єктів, звернення за межі виділеної пам'яті (out-of-bounds access), переповнення знакових цілих чисел, стани перегонів даних (data races), неправильні приведення типів (invalid casts), читання неініціалізованих даних, подвійне звільнення пам'яті (double frees) або порушення правил строгого псевдонімінгу (strict-aliasing violations). Засоби запобігання включають RAII (Resource Acquisition Is Initialization) та чітке проектування володіння (ownership) і часу життя, безпечніші абстракції та перевірки меж, тестування/фазинг, перегляд коду, статичний аналіз, а також використання санітайзерів, таких як ASan, UBSan і TSan.
7Поясніть різницю між невизначеною (undefined), неспецифікованою (unspecified) та залежною від реалізації (implementation-defined) поведінкою, навівши релевантні для бекенду приклади.
Невизначена поведінка (undefined behavior, UB) означає, що стандарт C++ не висуває жодних вимог; результати можуть включати аварійне завершення роботи, пошкодження даних, вразливості безпеки або залежну від оптимізатора неправильну компіляцію. Приклади: вихід за межі масиву (out-of-bounds access), використання після звільнення (use-after-free), переповнення знакових цілих чисел (signed integer overflow) та стани гонитви за даними (data races). Неспецифікована поведінка (unspecified behavior) означає, що стандарт допускає більше одного результату, і реалізація не зобов'язана документувати, який саме вибрано; поширений приклад — порядок обчислення багатьох аргументів функцій, тому код не повинен залежати від побічних ефектів, що обчислюються в певному порядку. Поведінка, залежна від реалізації (implementation-defined behavior), означає, що реалізація повинна вибрати та задокументувати поведінку; приклади включають наявність знаку у звичайного `char`, розміри/діапазони деяких фундаментальних типів у межах стандартних лімітів, а також деякі аспекти поведінки знакового зсуву вправо. У серверному коді UB — це ризик для коректності та безпеки, тоді як неспецифікована та залежна від реалізації поведінка — це ризики портованості, яких слід уникати або ізолювати в логіці протоколів, зберігання даних та кросплатформній взаємодії.
8Поясніть рівні безпеки винятків та те, як noexcept впливає на операції переміщення, контейнери та генерацію коду.
Рівні безпеки винятків (exception safety levels) описують, які твердження залишаються істинними, якщо операція викидає виняток. Базова гарантія (basic guarantee) означає, що інваріанти зберігаються, а ресурси не втрачаються, хоча стан міг змінитися. Строга гарантія (strong guarantee) означає семантику фіксації або відкоту (commit-or-rollback): у разі збою спостережуваний стан залишається незмінним. Гарантія відсутності винятків (nothrow guarantee) означає, що операція не викидає винятків. Поширені техніки включають RAII (Resource Acquisition Is Initialization), виконання роботи на тимчасових об'єктах, копіювання та обмін (copy-and-swap) і впорядкування змін таким чином, щоб фіксація відбувалася лише після успішного виконання операцій, що можуть викинути виняток. `noexcept` — це контракт: якщо функція з `noexcept` викидає виняток, викликається `std::terminate`. Це також впливає на узагальнений код і контейнери: наприклад, під час перерозподілу пам'яті (reallocation) `std::vector` може переміщувати елементи, коли конструктор переміщення є `noexcept`; в іншому випадку він може копіювати їх, часто за допомогою `std::move_if_noexcept`, щоб зберегти гарантії безпеки винятків. `noexcept` також може допомогти генерації коду або оптимізації, зменшуючи необхідні шляхи розповсюдження винятків, але лише тоді, коли обіцянка є правильною.
9Поясніть `std::expected` або еквівалентні типи результатів (outcome types) як альтернативу виняткам для операцій, що можуть завершитися помилкою.
`std::expected<T, E>` представляє або успішне значення `T`, або явну помилку `E`, роблячи збій частиною типу повернення функції замість покладання на поширення винятків. Це корисно для операцій, де помилки є очікуваними (fallible operations) й код, що їх викликає, повинен обробляти їх локально, наприклад: парсинг, валідація, мережеві виклики, пошук у сховищі та операції бекенду, які можна повторити. Тип помилки повинен містити структуровану інформацію, таку як код помилки, категорія, можливість повторної спроби, повідомлення або відображення на статуси HTTP (Hypertext Transfer Protocol) чи RPC (Remote Procedure Call). Типи очікуваних результатів (expected/outcome) дозволяють створювати композиції через перевірку та передачу помилок, а в API (Application Programming Interface) у стилі C++23 можуть використовувати монадичні операції, такі як `and_then`, `transform` та `or_else`, для створення ланцюжків кроків без глибоко вкладених умов. Порівняно з винятками, вони роблять потік керування та контракти API явними й добре працюють у середовищах без винятків або на межах ABI (Application Binary Interface); проте винятки можуть залишатися доречними для рідкісних, нелокальних або справді виняткових збоїв залежно від політики проєкту.
10Опишіть правила щодо часу життя (lifetime) для автоматичних, динамічних, тимчасових та асинхронно запитуваних об'єктів.
Автоматичні об'єкти існують з моменту створення до кінця своєї області видимості; вказівники або посилання на них стають недійсними (dangling) після виходу з цієї області. Динамічні об'єкти існують від моменту виділення пам'яті/створення доти, доки вони не будуть явно знищені, або поки їх не знищить об'єкт-власник; сирі вказівники (raw pointers) та посилання не продовжують час життя. Тимчасові об'єкти зазвичай існують до кінця повного виразу, зі специфічними правилами продовження часу життя при прив'язці до посилань, але це стосується не всіх випадків використання. Асинхронні задачі, такі як зворотні виклики (callbacks), потоки, таймери або співпрограми (coroutines), можуть виконуватися після того, як об'єкт, на який іде посилання, був знищений. Тому захоплення та збережені посилання потребують явного керування часом життя, щоб уникнути появи недійсних посилань і помилок використання після звільнення (use-after-free).
11Опишіть проблеми порядку статичної ініціалізації та як constinit, магічні статики (magic statics) і впровадження залежностей їх пом'якшують.
Проблеми з порядком статичної ініціалізації виникають через те, що динамічно ініціалізовані об'єкти на рівні простору імен (namespace-scope) або статичні об'єкти в різних одиницях трансляції (translation units) мають невизначений відносний порядок ініціалізації. Одна глобальна змінна може використовувати іншу до її створення; порядок знищення також може спричинити подібні проблеми під час завершення роботи програми. Ключове слово `constinit` вимагає, щоб статична або локальна для потоку (thread-local) змінна мала статичну/константну ініціалізацію, інакше програма вважатиметься некоректною (ill-formed); це дозволяє уникнути залежності від порядку динамічної ініціалізації для цієї змінної. Магічні статики (magic statics), або локальні для функції статики, ініціалізуються при першому використанні і є потокобезпечними починаючи з C++11. Впровадження залежностей (dependency injection) усуває приховані глобальні залежності шляхом створення об'єктів у контрольованому порядку та явної передачі залежностей.
12Що таке усунення копіювання (copy elision), NRVO (Named Return Value Optimization) та RVO (Return Value Optimization), і коли на них можна покладатися?
Усунення копіювання означає створення об'єкта безпосередньо в його кінцевому місці призначення замість створення окремого тимчасового об'єкта та його копіювання або переміщення. RVO зазвичай означає повернення неіменованого тимчасового об'єкта або `prvalue`, наприклад `return T{};`; у C++17 та новіших версіях багато з цих випадків є обов'язковими, оскільки `prvalue` ініціалізує результуючий об'єкт безпосередньо. NRVO — це повернення іменованого локального об'єкта, наприклад `return x;`; компілятору дозволено створювати цей локальний об'єкт безпосередньо в слоті повернення того, хто викликає функцію, але це не гарантується. Ви можете покладатися на обов'язкове усунення `prvalue` у C++17 у зазначених випадках, але не на те, що NRVO відбуватиметься завжди; уникайте використання `std::move` для іменованого локального значення, що повертається, оскільки це може запобігти NRVO.
13Як вимоги до компаратора, перевірки рівності та хешувальної функції впливають на коректність роботи асоціативних контейнерів?
Асоціативні контейнери покладаються на порівняння, перевірку рівності та хешування для визначення ідентичності ключів і підтримки своїх внутрішніх інваріантів. Упорядковані контейнери, такі як std::map, вимагають, щоб компаратор задавав строгий слабкий порядок (strict weak ordering); ключі вважаються еквівалентними, коли жоден із них не є меншим за інший, причому не обов'язково через operator==. Неупорядковані контейнери вимагають, щоб рівність була відношенням еквівалентності, і будь-які два ключі, які вважаються рівними, повинні повертати однакове значення хешу. Ключі не можна змінювати під час їх зберігання таким чином, щоб це змінювало їхній порядок, рівність або хеш, оскільки це може зробити поведінку пошуку, видалення та забезпечення унікальності некоректною.
14Поясніть гетерогенний пошук у впорядкованих та невпорядкованих асоціативних контейнерах.
Гетерогенний пошук дозволяє здійснювати пошук в асоціативному контейнері за допомогою типу, що відрізняється від типу його ключа, уникаючи тимчасового конструювання ключа. У впорядкованих контейнерах це вимагає прозорого компаратора (transparent comparator), наприклад `std::less<>` або користувацького компаратора з маркером `is_transparent`. У невпорядкованих контейнерах це вимагає як прозорого хешування, так і прозорих функторів рівності, які можуть узгоджено обробляти тип ключа та тип пошуку. Типовим прикладом для беккенду є контейнер із ключем `std::string`, у якому можна здійснювати пошук за допомогою `std::string_view` або `const char*` без виділення пам'яті під тимчасовий `std::string`.
15Поясніть модель пам'яті C++: стан гонитви (data races), відношення «відбувається до» (happens-before) та гарантії синхронізації.
Модель пам'яті C++ визначає, коли операції в різних потоках упорядковуються та коли результати запису стають видимими. Стан гонитви виникає, коли два потоки одночасно звертаються до однієї ділянки пам'яті, принаймні одне зі звернень є записом, і ці звернення не впорядковані за допомогою happens-before або не зроблені безпечними завдяки атомарним операціям; стан гонитви призводить до невизначеної поведінки. Happens-before — це відношення впорядкування, яке робить попередні побічні ефекти видимими для наступних операцій. Операції синхронізації створюють це впорядкування: наприклад, розблокування м'ютекса створює відношення synchronizes-with з наступним успішним блокуванням того ж м'ютекса, а відповідні атомарні операції release/acquire можуть синхронізувати виконання між потоками. Коректні програми використовують м'ютекси, атомарні змінні або інші засоби синхронізації для встановлення happens-before щодо спільного стану.