Przygotowanie do rozmowy C++

Pytania rekrutacyjne C++ Backend Developer

Wyselekcjonowane pytania rekrutacyjne C++ dla programistów backend, pogrupowane tematycznie i generowane z tego samego katalogu pytań, który zasila platformę EngineerSpeak.

Rozpocznij próbną rozmowę AI C++ BackendKarta płatnicza nie jest wymagana. Dostępna 1 darmowa sesja.
Ćwiczenie rozmów technicznych po angielskuTryb dla osób, dla których angielski nie jest językiem ojczystym, do ćwiczenia rozmów technicznych.

Resource Management

1Wyjaśnij paradygmat RAII (Resource Acquisition Is Initialization) i sposób, w jaki kształtuje on bezpieczne ze względu na wyjątki zarządzanie zasobami w usługach backendowych pisanych w C++.

RAII (Resource Acquisition Is Initialization) oznacza, że obiekt C++ posiada prawo własności do zasobu i zwalnia ten zasób w swoim destruktorze. Ponieważ obiekty lokalne są niszczone automatycznie w momencie, gdy kończy się ich czas życia (lifetime) i zakres, co ma również miejsce podczas zwijania stosu przy wywołaniu wyjątku (stack unwinding), RAII zapewnia deterministyczne zwalnianie zasobów i sprawia, że ścieżki błędów stają się bezpieczne w kontekście wyjątków (exception-safe). W usługach backendowych dotyczy to nie tylko pamięci, ale także deskryptorów plików, gniazd (sockets), blokad muteksów, uchwytów do baz danych, transakcji oraz innych zasobów operacyjnych lub systemowych aplikacji.

Odpowiedz na to pytanie z trenerem AI

Zarządzanie pamięcią

2Porównaj `std::unique_ptr` i `std::shared_ptr` oraz opisz, kiedy zastosowanie każdego z nich jest odpowiednie w backendowym API (Application Programming Interface).

`std::unique_ptr` reprezentuje wyłączną własność: jest mało kosztowny, można go przenosić, ale nie kopiować, i jest odpowiedni w przypadku pojedynczego właściciela lub dla API przenoszących prawo własności. `std::shared_ptr` reprezentuje współdzieloną własność: jest kopiowalny i utrzymuje obiekt przy życiu poprzez zliczanie referencji, dopóki nie zwolni go ostatni silny właściciel. W backendowym API używaj `unique_ptr` w przypadku przeniesienia własności, `shared_ptr` tylko wtedy, gdy wielu niezależnych właścicieli musi przedłużyć czas życia, a referencji lub surowych wskaźników do dostępu bez przekazania prawa własności. `std::make_shared` jest zazwyczaj preferowane przy tworzeniu obiektów `shared_ptr`, ponieważ jest wydajne i odporne na wyjątki (exception-safe).

Odpowiedz na to pytanie z trenerem AI

3Jak w inteligentnych wskaźnikach (smart pointers) działają niestandardowe funkcje zwalniające zasób (custom deleters) i kiedy są one przydatne w przypadku interoperacyjności między C a C++?

Niestandardowa funkcja zwalniająca zasób (custom deleter) to wywoływalna logika sprzątająca, z której korzysta inteligentny wskaźnik w miejsce domyślnej operacji delete w momencie zwalniania posiadanego zasobu. Jest to przydatne w kontekście interoperacyjności pomiędzy językami C i C++, gdy zasób musi zostać zwolniony za pomocą konkretnej funkcji, takiej jak fclose, close, curl_easy_cleanup, SSL_free, free lub dedykowanej funkcji niszczącej dla danej biblioteki. W przypadku unique_ptr typ funkcji zwalniającej zasób jest częścią typu unique_ptr i może wpływać na jego rozmiar; bezstanowe funkcje zwalniające mogą zostać usunięte w procesie optymalizacji, podczas gdy wskaźniki na funkcje lub stanowe funkcje zwalniające alokują dodatkowe miejsce. W przypadku shared_ptr funkcja zwalniająca zasób jest przechowywana w bloku kontrolnym (control block) i wykonuje się, gdy ostatni silny właściciel (strong owner) zwalnia obiekt. Niestandardowe funkcje zwalniające zasób pozwalają na bezpieczne wykorzystywanie zasobów pochodzących z języka C zgodnie z paradygmatem RAII (Resource Acquisition Is Initialization).

Odpowiedz na to pytanie z trenerem AI

4Wyjaśnij mechanikę bloków kontrolnych (control blocks) dla `weak_ptr` oraz `shared_ptr`, uwzględniając cykle referencyjne, `enable_shared_from_this` oraz koszty związane z licznikami referencji.

Obiekt `shared_ptr` zarządza współdzieloną własnością za pośrednictwem bloku kontrolnego, który zawiera liczniki silnych i słabych referencji oraz informacje o czyszczeniu, takie jak niestandardowa funkcja zwalniająca zasób (custom deleter) oraz alokator. Kopiowanie lub niszczenie obiektów `shared_ptr` inkrementuje lub dekrementuje licznik silnych referencji, zazwyczaj za pomocą operacji atomowych, dzięki czemu poszczególne obiekty `shared_ptr` mogą być bezpiecznie manipulowane przez różne wątki, jednak każda aktualizacja licznika referencji niesie ze sobą koszt wydajnościowy. Kiedy licznik silnych referencji osiągnie zero, zarządzany obiekt jest niszczony; blok kontrolny pozostaje w pamięci do momentu, aż znikną również wszystkie słabe referencje. `weak_ptr` wskazuje na ten sam blok kontrolny, nie wydłużając przy tym czasu życia obiektu; metoda `lock()` zwraca `shared_ptr`, jeśli obiekt nadal żyje, lub pusty `shared_ptr` w przeciwnym razie. Cykle zbudowane wyłącznie z `shared_ptr` powodują wycieki pamięci, ponieważ liczniki silnych referencji nigdy nie osiągają zera, dlatego `weak_ptr` stosuje się do wskaźników wstecznych lub w relacjach obserwatora. Funkcja `enable_shared_from_this` pozwala obiektowi będącemu już pod własnością `shared_ptr` utworzyć nowy `shared_ptr` wskazujący na samego siebie z użyciem istniejącego bloku kontrolnego, co pozwala uniknąć niebezpieczeństwa utworzenia oddzielnych bloków kontrolnych. Koszty związane z licznikami referencji obejmują atomową inkrementację/dekrementację, rywalizację o pamięć podręczną (cache contention), alokację bloku kontrolnego oraz narzut wydajnościowy w fragmentach kodu krytycznych dla wydajności.

Odpowiedz na to pytanie z trenerem AI

System typów

5Czym jest semantyka przeniesienia (move semantics) i w jaki sposób implementuje się poprawny konstruktor przenoszący oraz przenoszący operator przypisania?

Semantyka przeniesienia (move semantics) pozwala w C++ przetransferować zasoby z obiektów tymczasowych lub w inny sposób zbędnych zamiast ich kopiowania. Wykorzystuje referencje do r-wartości, takie jak `T&&`, oraz `std::move`, co jest po prostu rzutowaniem, które pozwala wybrać przeciążenia dla przenoszenia; `std::move` samo w sobie niczego nie przenosi. Poprawny konstruktor przenoszący inicjuje nowy obiekt, przejmując zasób obiektu źródłowego i pozostawiając obiekt źródłowy w stanie ważnym, pozwalającym na zniszczenie i przypisanie. Poprawny przenoszący operator przypisania transferuje dane do już istniejącego obiektu, obsługuje lub toleruje przypisanie do samego siebie, zwalnia lub ponownie używa bieżącego zasobu obiektu docelowego, przejmuje zasób z obiektu źródłowego i pozostawia obiekt źródłowy w bezpiecznym stanie. Operacje przenoszenia powinny być często oznaczane jako `noexcept`, aby kontenery standardowe mogły ich używać podczas realokacji z zachowaniem gwarancji wyjątków.

Odpowiedz na to pytanie z trenerem AI

6Opisz poprawność użycia const (const-correctness) w interfejsach API (Application Programming Interface) C++ i jak skutecznie projektować stałe funkcje składowe (const member functions).

Poprawność użycia const (const-correctness) polega na zadeklarowaniu poprzez system typów, które operacje nie modyfikują obserwowalnego ani logicznego stanu obiektu. Stała funkcja składowa korzysta z obiektu `this` ograniczonego modyfikatorem `const`, zatem nie może modyfikować niemutowalnych pól w klasie ani wywoływać niestałych (non-const) funkcji składowych na tym samym obiekcie. Prawidłowe podejście do projektowania API narzuca oznaczanie zapytań tylko do odczytu słowem `const`, zwracanie wartości po stałych referencjach/wskaźnikach w stosownych sytuacjach i unikanie eksponowania mutowalnego, wewnętrznego stanu z funkcji ujętych jako `const`. Słowo kluczowe `mutable` powinno być rezerwowane dla detali implementacyjnych niezmieniających stanu logicznego – do cache'owania, wartości leniwie inicjowanych (lazy values), metryk lub muteksów. Zastosowanie modyfikatora `const` stwarza kontrakt określający mutowalność, a nie zautomatyzowaną gwarancję bezpieczeństwa dla wątków; te ostatnie wymagają odrębnej implementacji i odpowiedniej dokumentacji.

Odpowiedz na to pytanie z trenerem AI

7Czym są typy wyłącznie do przenoszenia (move-only types) i jak wpływają one na granice API dla gniazd, uchwytów plików i blokad?

Typy wyłącznie do przenoszenia (move-only types) to typy, których nie można kopiować, ale można je przenosić. Są one powszechne w przypadku unikalnej własności zasobów, takich jak gniazda (sockets), deskryptory plików, uchwyty plików, blokady oraz `std::unique_ptr`. Operacje kopiowania są usunięte (deleted), aby uniknąć duplikacji własności i podwójnego zwalniania zasobów; operacje przenoszenia przekazują własność i pozostawiają obiekt źródłowy w prawidłowym stanie, ale zazwyczaj pusty lub bez przypisanego zasobu. API (Application Programming Interface) powinno w jawny sposób określać granice własności: fabryki mogą zwracać obiekty przenośne przez wartość, funkcje przejmujące własność mogą przyjmować je przez wartość lub jako referencję do r-wartości (rvalue reference), a funkcje służące tylko do inspekcji powinny przyjmować referencje, wskaźniki lub inne mechanizmy pożyczające zasób (borrowing handles). Kontenery mogą przechowywać wartości wyłącznie do przenoszenia, gdy elementy są wstawiane lub relokowane przy użyciu operacji przeniesienia.

Odpowiedz na to pytanie z trenerem AI

8Porównaj auto, decltype, decltype(auto) oraz dedukcję argumentów szablonu w typowym kodzie backendowym.

Słowo kluczowe `auto` używa dedukcji podobnej do szablonów dla zmiennych: zwykłe `auto` przeważnie odrzuca referencje oraz najwyższego poziomu modyfikatory `const`, chyba że deklaracja wymaga ich bezpośrednio, na przykład `auto&`, `const auto&` lub `auto&&`. Instrukcja `decltype(expr)` znacznie dokładniej sprawdza zadeklarowany typ lub typ wyrażenia: wyrażenie oparte na identyfikatorze bez nawiasów (id-expression) zwraca zadeklarowany typ, podczas gdy inne wyrażenia zwracające l-wartości dają `T&`, x-wartości dają `T&&`, a pr-wartości dają `T`. Zapis `decltype(auto)` dokonuje dedukcji przy użyciu reguł `decltype`, często wykorzystywanych w typach zwracanych z funkcji, gdy konieczne jest zachowanie referencji. Dedukcja argumentów szablonu działa podobnie do `auto`, ale zależy od formy parametru (np. `T`, `T&`, `const T&` czy `T&&`) i ma własne zasady. Typową różnicą są listy inicjalizacyjne w nawiasach klamrowych: instrukcja `auto x = {1,2}` dedukuje `std::initializer_list<int>`, podczas gdy w przypadku zwykłego parametru szablonu zazwyczaj nie można wydedukować typu `T` z samej listy inicjalizacyjnej, o ile parametr nie oczekuje `initializer_list` lub innego odpowiedniego typu.

Odpowiedz na to pytanie z trenerem AI

Czas życia obiektów

9Opisz Zasadę Zera, Zasadę Trzech i Zasadę Pięciu, a także określ, kiedy każda z nich ma zastosowanie.

Zasada Zera (Rule of Zero): preferuj klasy, które nie deklarują niestandardowego destruktora ani operacji kopiowania/przenoszenia; pozwól członkom wykorzystującym idiom RAII (Resource Acquisition Is Initialization), takim jak `std::string`, `std::vector`, `std::unique_ptr`, opakowaniom plików/gniazd itp., zarządzać zasobami. Zasada Trzech (Rule of Three): jeśli klasa samodzielnie zarządza zasobem i potrzebuje własnego destruktora, konstruktora kopiującego lub kopiującego operatora przypisania, zazwyczaj potrzebuje wszystkich trzech, aby zdefiniować poprawne zachowanie podczas kopiowania oraz prawo własności. Zasada Pięciu (Rule of Five): w języku C++11 i nowszych, takie typy powinny również brać pod uwagę konstruktor przenoszący oraz przenoszący operator przypisania. Używaj Zasady Zera w przypadku większości typów aplikacyjnych; używaj Zasady Trzech/Pięciu, gdy typ jest bezpośrednim właścicielem zasobu lub posiada nietrywialną semantykę własności/czasu życia.

Odpowiedz na to pytanie z trenerem AI

Semantyka języka

10Opisz formy inicjalizacji w języku C++ oraz powszechne pułapki z nimi związane, uwzględniając typ std::initializer_list i agregaty (aggregates).

W C++ istnieje kilka form inicjalizacji. Inicjalizacja domyślna (default initialization), np. `T x;`, wywołuje domyślny konstruktor dla typów klasowych, ale pozostawia lokalne zmienne wbudowanych typów podstawowych (automatic fundamental variables) bez zainicjowanej wartości. Inicjalizacja wartością (value initialization), np. `T x{};` lub `T()`, inicjalizuje zmienne zerami, jeśli to możliwe, przed wywołaniem konstruktora lub zainicjalizowaniem składowych. Inicjalizacja listowa (list initialization) używa nawiasów klamrowych, zakazuje stosowania konwersji zawężających (narrowing conversions) i posiada specjalne reguły rozwiązywania przeciążeń (overload resolution), w tym wymusza silną preferencję dla konstruktorów przyjmujących `std::initializer_list`. Inicjalizacja agregatów (aggregate initialization) przypisuje wartości składowym bezpośrednio przy użyciu klamer; od wersji C++20 obsługiwane są również dedykowane inicjalizatory (designated initializers) działające w oparciu o kolejność deklaracji. Najczęstsze pułapki to niezainicjalizowane lokalne typy skalarne, zaskakujące wybory przeciążeń przy `initializer_list`, błędy zawężania typów w klamrach, najbardziej kłopotliwy błąd parsowania (the most vexing parse) przy stosowaniu okrągłych nawiasów oraz zmiany w zachowaniu programu, gdy typ przestaje być agregatem.

Odpowiedz na to pytanie z trenerem AI

11Czym jest niezdefiniowane zachowanie (undefined behavior) w C++ i jak może objawiać się w postaci incydentów na produkcji w kodzie backendowym?

Niezdefiniowane zachowanie (undefined behavior - UB) to zachowanie, co do którego po dokonaniu nieprawidłowej operacji standard języka C++ nie nakłada w zasadzie żadnych ścisłych wymagań. Pogram wykazujący to zjawisko może pozornie funkcjonować prawidłowo, ulegać awariom, uszkadzać zawartość w pamięci, ujawniać błędy związane z bezpieczeństwem bądź podlegać optymalizacjom skutkującymi nieoczekiwanym skutkiem końcowym. Kompilatory zakładają zwyczajnie, że zjawisko nie występuje, więc dobierają formę kompilacji adekwatnie, dlatego też usterki bywają dostrzegane dopiera w skompilowanych programach typu release czy po uruchomieniu ruchu na produkcji. Błędy backendowe wynikają niekiedy z istnienia wiszących wskaźników/referencji, odwołań do już wyczyszczonej pamięci (use-after-free), naruszenia cyklu czasu życia obiekty (object lifetime violations), czytników odczytujących z miejsc po za wyznaczoną granicą bufora, tzw. przepełnienia całkowitoliczbowego na typach ze znakiem, wyścigu o zasoby po stronie danych (data races), nieważnych rzutowań, podwójnego usunięcia czy też nieuprawnionych odwołań naruszających tzw. strict-aliasing. Obniżenie ryzyka wymusza bazowanie na RAII (Resource Acquisition Is Initialization), czytelnym zarządzaniu prawami własności/cyklem obiektu z jednoczesnym wprowadzeniu testów fuzzingu, audytu, sprawdzaniu granic (bounds checks) ze stosownym oparciem się np. na tzw. sanitizerach takich jak narzędzia typu ASan, UBSan, TSan.

Odpowiedz na to pytanie z trenerem AI

Object Model

12Wyjaśnij pojęcia funkcji wirtualnych, tablic metod wirtualnych (vtables), kosztu dynamicznego wiązania (dynamic dispatch), obcinania obiektów (object slicing) oraz zagrożeń związanych z wirtualnymi destruktorami.

Funkcja wirtualna umożliwia polimorfizm w czasie wykonywania: w przypadku wywołania poprzez wskaźnik lub referencję na klasę bazową, wybierana jest implementacja właściwa dla typu dynamicznego obiektu. Większość implementacji przechowuje w każdym obiekcie polimorficznym ukryty wskaźnik `vptr`, który wskazuje na tablicę adresów funkcji wirtualnych (tzw. `vtable`) powiązanych z danym typem dynamicznym. Typowym obciążeniem kosztowym takiego rozwiązania jest przechowywanie dodatkowego wskaźnika w samym obiekcie, wykonanie połączeń pośrednich, jak i możliwy negatywny wpływ na efektywność mechanizmu pamięci podręcznej oraz działania przewidywania skoków (branch-prediction). Co więcej, osłabia to możliwości wstawiania kodu w miejscu jego wywołania (inlining), choć współczesne kompilatory niekiedy potrafią obejść te ograniczenia przez proces znany jako dewirtualizacja. Z kolei mechanizm znany jako obcinanie obiektów (object slicing) zachodzi z chwilą, gdy dany obiekt będący klasą pochodną jest skopiowany lub zapisany przez wartość w ramy zwykłego obiektu należącego do klasy bazowej. Traci przy tym zarówno specyficzną część definicji klasy pochodnej, jak i powiązane z nią własności o charakterze dynamicznym. Jeżeli jakakolwiek klasa bazowa ma być usunięta przy użyciu operowania jej wskaźnikiem, wymagane jest, aby jej własny destruktor również przyjął formę funkcji wirtualnej; w przeciwnym razie próba usunięcia powiązanego z takowym wskaźnikiem obiektu będącego pochodnym skutkuje wystąpieniem niezdefiniowanego zachowania (undefined behavior).

Odpowiedz na to pytanie z trenerem AI

Biblioteka standardowa

13Opisz zegary, punkty w czasie i czasy trwania z biblioteki `std::chrono` w kontekście limitów czasu (timeouts) usług, metryk i znaczników czasu (timestamps).

`std::chrono` modeluje czas za pomocą zegarów (clocks), punktów w czasie (`time_point`) i czasów trwania (`duration`). Czas trwania (`duration`) to interwał przypisany do jednostki, na przykład milisekund lub sekund. Punkt w czasie (`time_point`) to określony moment na osi czasu konkretnego zegara. Zegar `steady_clock` jest monotoniczny i powinien być używany do mierzenia czasu, który upłynął, limitów czasu usług (timeouts), terminów wykonania (deadlines) oraz pomiarów opóźnień (latency), ponieważ zmiany czasu systemowego (wall-clock) nie mają na niego wpływu. Zegar `system_clock` reprezentuje czas systemowy/obywatelski i jest odpowiedni dla znaczników czasu (timestamps), logowania, utrwalania danych i konwersji kalendarzowych, ale może przeskoczyć w wyniku korekty czasu w systemie operacyjnym. Limity czasu powinny zazwyczaj opierać się na `steady_clock::now() + duration`; znaczniki czasu generowane maszynowo powinny używać jasno udokumentowanego formatu czasu, zazwyczaj UTC (Coordinated Universal Time) na granicach API (Application Programming Interface) i w warstwie przechowywania danych.

Odpowiedz na to pytanie z trenerem AI

14Opisz std::format i nowoczesne mechanizmy formatowania w porównaniu do iostreams oraz interfejsów API (Application Programming Interface) w stylu printf.

`std::format` to wprowadzone w C++20 bezpieczne typologicznie narzędzie do formatowania, zainspirowane biblioteką fmtlib. Wykorzystuje pola zastępcze `{}` i specyfikacje formatu do generowania sformatowanego tekstu, bez konieczności używania składni wstawiania iostreams (zachowującej stan) ani argumentów ze zmienną liczbą parametrów z języka C w stylu `printf`. W porównaniu do `printf`, zapobiega to wielu problemom związanym z niedopasowaniem typu do formatu; w porównaniu do iostreams jest często czytelniejsze i łatwiejsze w komponowaniu. fmtlib to powszechnie używana biblioteka, która poprzedziła i wpłynęła na `std::format`, i może oferować szersze wsparcie lub nowsze funkcje. Typy zdefiniowane przez użytkownika mogą być formatowane przy wsparciu niestandardowych formaterów. Przy logowaniu o dużej objętości, wydajność zależy od unikania niepotrzebnego formatowania, konwersji oraz alokacji, szczególnie w przypadku wyłączonych poziomów logowania; preferowane są interfejsy API, które odraczają formatowanie lub najpierw sprawdzają poziom logowania.

Odpowiedz na to pytanie z trenerem AI

Szablony

15Wyjaśnij kategorie wartości w języku C++ oraz doskonałe przekazywanie (perfect forwarding), a także dlaczego są one ważne dla wydajnych, generycznych API.

Kategorie wartości w C++ opisują wyrażenia: l-wartości (lvalues) posiadają tożsamość i można się do nich odnosić po zakończeniu wyrażenia; pr-wartości (prvalues) to czyste r-wartości (pure rvalues), takie jak wiele obiektów tymczasowych czy wartości wyliczonych; x-wartości (xvalues) to wygasające obiekty, których zasoby mogą zostać ponownie wykorzystane. Doskonałe przekazywanie (perfect forwarding) to technika szablonowa polegająca na przyjmowaniu referencji przekazującej (forwarding reference), zwykle `T&&`, gdzie `T` jest dedukowane, i przekazywaniu wartości dalej za pomocą `std::forward<T>(arg)`. Dzięki temu kategoria wartości użyta przez wywołującego zostaje zachowana: l-wartości pozostają l-wartościami, a r-wartości pozostają r-wartościami. Reguły zwijania referencji (reference collapsing rules) sprawiają, że jest to możliwe. Ma to znaczenie w przypadku generycznych API backendowych, ponieważ wrappery, fabryki, dyspozytory i funkcje typu emplace mogą unikać niepotrzebnych kopii, zachowując przy tym możliwość doboru odpowiedniego przeciążenia oraz wywołania semantyki przeniesienia (move semantics).

Odpowiedz na to pytanie z trenerem AI

Współbieżność

16Opisz strategie zapobiegania zakleszczeniom (deadlock) i blokowania wielu muteksów przy użyciu std::scoped_lock i std::lock.

Zakleszczeniom zapobiega się poprzez unikanie oczekiwań cyklicznych (circular waits): w miarę możliwości należy zakładać blokady w spójnej, globalnej kolejności lub blokować wiele muteksów za pomocą std::lock/std::scoped_lock, które wykorzystują algorytm unikania zakleszczeń. Konstrukcja std::scoped_lock lock(a, b, ...) jest najprostszą formą w ramach wzorca RAII (Resource Acquisition Is Initialization) do zablokowania kilku muteksów i ich automatycznego odblokowania przy wyjściu z zakresu. W przypadku std::lock, należy najpierw zablokować muteksy, a następnie dołączyć wrappery RAII przy użyciu flagi std::adopt_lock, albo użyć std::unique_lock z flagą std::defer_lock. Utrzymuj sekcje krytyczne jak najkrótsze i unikaj operacji blokujących lub nieznanych wywołań zwrotnych (callbacks) podczas trzymania blokad; unikanie zakleszczeń nie gwarantuje automatycznie sprawiedliwości (fairness) ani nie zapobiega wszystkim scenariuszom typu livelock lub zagłodzenie (starvation).

Odpowiedz na to pytanie z trenerem AI

17Wyjaśnij wzorce użycia `condition_variable` do oczekiwania na zmiany stanu.

Używaj `std::condition_variable` do oczekiwania na zmianę predykatu stanu chronionego przez muteks. Współdzielony stan jest modyfikowany podczas zajmowania tego samego muteksu, a następnie `notify_one` lub `notify_all` budzi oczekujące wątki. Oczekujące wątki powinny korzystać z `cv.wait(lock, predicate)` lub równoważnej pętli, ponieważ wybudzenia mogą być fałszywe, a powiadomienia nie są zapamiętywane niezależnie od stanu predykatu. `notify_one` budzi jeden oczekujący wątek; `notify_all` budzi wszystkie oczekujące wątki i jest odpowiednie dla zmian stanu wymagających rozgłoszenia, takich jak zamykanie aplikacji.

Odpowiedz na to pytanie z trenerem AI

18Zaprojektuj bezpieczną wątkowo, ograniczoną kolejkę, używając standardowych mechanizmów C++ i określ gwarancje jej API (Application Programming Interface).

Ograniczona i bezpieczna wątkowo kolejka może zostać zbudowana przy użyciu `std::mutex`, zmiennych warunkowych, kontenera o stałej pojemności oraz flagi zamknięcia. Operacja `push` blokuje, przekracza limit czasu lub kończy się niepowodzeniem, gdy kolejka jest pełna, co zapewnia mechanizm przeciwciśnienia; operacja `pop` blokuje, gdy kolejka jest pusta. Obie operacje powinny oczekiwać na predykaty takie jak `size < capacity || closed` oraz `!empty || closed`. Przy zamykaniu wybudź zablokowanych producentów i konsumentów; odrzuć nowe operacje `push` i określ, czy konsumenci mają opróżnić kolejkę z istniejących elementów, czy natychmiast się zatrzymać. API powinno precyzować zachowanie przy blokowaniu, semantykę zamknięcia, wartości zwracane i gwarancje współbieżności.

Odpowiedz na to pytanie z trenerem AI

Database

19Jak przeprowadzić migrację schematu bazy danych dla usługi w C++ bez przerw w dostępności (downtime)?

Zastosuj wieloetapową migrację typu rozszerz-i-zwiń (expand-contract). Najpierw wprowadź wstecznie kompatybilne rozszerzenia schematu, takie jak kolumny dopuszczające wartość null lub nowe tabele, które nie zaburzają działania obecnie uruchomionej usługi w C++. Wdróż kompatybilny kod, który potrafi obsłużyć zarówno starą, jak i nową reprezentację danych, uzupełnij istniejące dane (backfill) w małych, ograniczonych partiach, zweryfikuj spójność, przełącz operacje odczytu na nowy schemat, a dopiero na końcu usuń stare kolumny lub kod, gdy wszystkie wdrożone wersje przestaną od nich zależeć. W przypadku dużych tabel unikaj długich, blokujących operacji DDL (Data Definition Language), używaj operacji online/współbieżnych tam, gdzie są wspierane, monitoruj blokady/replikację/zasoby oraz utrzymuj ścieżki wycofywania zmian (rollback), które działają z częściowo wdrożonym kodem i częściowo zmigrowanymi danymi.

Odpowiedz na to pytanie z trenerem AI

20Jak usługa w języku C++ powinna wykonywać odczyty z replik bazy danych, zarządzając jednocześnie nieaktualnymi odczytami (stale reads) i gwarancjami odczytu własnych zapisów (read-your-writes)?

Traktuj odczyty z replik jako jawny kompromis w kwestii spójności. Odczyty tolerujące opóźnione dane (staleness) mogą być kierowane do zdrowych replik, jednak operacje typu 'odczyt własnych zapisów' (read-your-writes), odczyty transakcyjne lub krytyczne pod względem aktualności danych powinny trafiać do instancji głównej (primary), chyba że wiadomo, że wybrana replika odtworzyła określoną pozycję zapisu, np. identyfikowaną przez LSN (Log Sequence Number), znacznik czasu (timestamp) lub wersję. Zadbaj o śledzenie opóźnień (replica lag) i statusu poprawności działania replik, ustalaj wymaganą spójność na poziomie punktów końcowych (endpoints) lub żądań i powracaj do odpytywania bazy głównej, oczekuj na nadrobienie replikacji lub zgłaszaj błąd, gdy opóźnienia przekroczą dozwolone granice. Udokumentuj model spójności po to, by podmioty odpytujące (callers) były świadome, które spośród zapytań mogą obejmować nieaktualne dane.

Odpowiedz na to pytanie z trenerem AI