Příprava pro Middle C++ vývojáře

Otázky na pohovor pro Middle C++ backend vývojáře

15 vybraných otázek na pohovor v C++ pro middle backend vývojáře, kteří potřebují diskutovat o výkonu, vlastnictví a kompromisech v produkčním prostředí.

Spustit Middle C++ AI pohovorKreditní karta není potřeba. K dispozici je 1 bezplatná lekce.
Procvičování technických pohovorů v angličtiněRežim pro lidi, pro které angličtina není rodným jazykem, určený k procvičování technických pohovorů.

Správa paměti

1Jak u chytrých ukazatelů (smart pointers) fungují vlastní funkce pro uvolnění prostředku (custom deleters) a kdy jsou užitečné pro interoperabilitu jazyků C a C++?

Vlastní funkce pro uvolnění prostředku (custom deleter) je volatelná čisticí logika, kterou chytrý ukazatel použije místo výchozí operace delete, když uvolňuje jím vlastněný prostředek. Je to užitečné pro interoperabilitu C/C++, když musí být prostředek uvolněn konkrétní funkcí, jako je fclose, close, curl_easy_cleanup, SSL_free, free nebo destrukční funkce knihovny. U unique_ptr je typ funkce pro uvolnění prostředku součástí samotného typu unique_ptr a může ovlivnit jeho velikost; bezstavové funkce mohou být při optimalizaci vynechány, zatímco ukazatele na funkce nebo stavové funkce pro uvolnění prostředku přidávají nároky na paměť. U shared_ptr je funkce pro uvolnění prostředku uložena v řídicím bloku (control block) a spustí se, když objekt uvolní poslední silný vlastník (strong owner). Vlastní funkce pro uvolnění prostředku umožňují prostředkům z jazyka C bezpečně se začlenit do konceptu RAII (Resource Acquisition Is Initialization).

Zkusit odpovědět na tuto otázku s trenérem AI

2Vysvětlete mechanismy řídicího bloku (control block) u `weak_ptr` a `shared_ptr`, včetně cyklů, `enable_shared_from_this` a nákladů na čítač referencí (refcount).

`std::shared_ptr` spravuje sdílené vlastnictví pomocí řídicího bloku, který obsahuje počty silných a slabých referencí a informace o úklidu, jako je funkce pro uvolnění prostředku (custom deleter) a alokátor. Kopírování nebo zničení objektů `shared_ptr` inkrementuje nebo dekrementuje silný počet, obvykle pomocí atomických operací, takže s oddělenými objekty `shared_ptr` lze bezpečně manipulovat napříč vlákny, ale každá aktualizace čítače referencí něco stojí. Když silný počet klesne na nulu, spravovaný objekt je zničen; řídicí blok zůstává v paměti, dokud nezmizí i všechny slabé reference. `std::weak_ptr` ukazuje na stejný řídicí blok, aniž by prodlužoval životnost objektu; volání `lock()` vrátí `shared_ptr`, pokud je objekt stále naživu, v opačném případě vrátí prázdný `shared_ptr`. U cyklů tvořených pouze pomocí `shared_ptr` dochází k únikům paměti (memory leak), protože silné počty nikdy nedosáhnou nuly, proto se `weak_ptr` používá pro zpětné ukazatele nebo vazby u návrhového vzoru pozorovatel (observer). `enable_shared_from_this` umožňuje objektu, který je již vlastněn pomocí `shared_ptr`, vytvořit nový `shared_ptr` na sebe sama s použitím stávajícího řídicího bloku, čímž se vyhne nebezpečí vytvoření oddělených řídicích bloků. Náklady na počítání referencí zahrnují atomické inkrementace/dekrementace, boj o cache (cache contention), alokaci řídicího bloku a režii v často vykonávaných částech kódu kritických z hlediska výkonu.

Zkusit odpovědět na tuto otázku s trenérem AI

Šablony

3Vysvětlete kategorie hodnot v C++ a dokonalé předávání (perfect forwarding) a proč jsou důležité pro efektivní generická rozhraní API (Application Programming Interface).

Kategorie hodnot v C++ popisují výrazy: `lvalues` mají identitu a lze se na ně odkazovat i po skončení výrazu; `prvalues` jsou čisté r-hodnoty, jako je mnoho dočasných objektů nebo vypočítaných hodnot; `xvalues` jsou objekty, jejichž platnost končí (expiring objects) a jejich prostředky mohou být znovu využity. Dokonalé předávání (perfect forwarding) je šablonová technika, při které se přijímá předávací reference (forwarding reference), typicky `T&&`, kde je `T` dedukováno, a předává se pomocí `std::forward<T>(arg)` tak, aby byla zachována kategorie hodnoty volajícího: `lvalues` zůstávají `lvalues` a `rvalues` zůstávají `rvalues`. Toto je možné díky pravidlům kolapsu referencí (reference collapsing). Pro generická backendová rozhraní API je to důležité, protože obalující funkce (wrappers), továrny (factories), dispečery a funkce stylu emplace se tak mohou vyhnout zbytečným kopiím a zachovat výběr přetížení (overload resolution) a sémantiku přesunu (move behavior).

Zkusit odpovědět na tuto otázku s trenérem AI

Typový systém

4Porovnejte `auto`, `decltype`, `decltype(auto)` a dedukci parametrů šablony v běžném backendovém kódu.

Klíčové slovo `auto` používá pro proměnné dedukci podobnou šablonám: prosté `auto` obvykle odstraňuje reference a `const` na nejvyšší úrovni (top-level const), pokud si je deklarace přímo nevyžádá, například `auto&`, `const auto&` nebo `auto&&`. Výraz `decltype(expr)` zkoumá deklarovaný typ nebo typ výrazu přesněji: id-výraz bez závorek vrací deklarovaný typ, zatímco jiné výrazy typu l-hodnoty (lvalue) produkují `T&`, x-hodnoty (xvalue) produkují `T&&` a pr-hodnoty (prvalue) produkují `T`. `decltype(auto)` provádí dedukci pomocí pravidel `decltype` a často se používá pro návratové typy, když musí být zachovány reference. Dedukce parametrů šablony je podobná jako `auto`, ale závisí na formě parametru, například `T`, `T&`, `const T&` nebo `T&&`, a má svá vlastní pravidla. Běžným rozdílem jsou inicializátory ve složených závorkách: `auto x = {1,2}` vydedukuje typ `std::initializer_list<int>`, zatímco prostý parametr šablony obecně nedokáže vydedukovat `T` z holého inicializátoru ve složených závorkách, pokud parametr neočekává `initializer_list` nebo jiný vhodný typ.

Zkusit odpovědět na tuto otázku s trenérem AI

5Porovnejte std::variant s polymorfismem založeným na dědičnosti pro modelování heterogenních zpráv nebo událostí.

std::variant je hodnotový typ, který obsahuje přesně jednu alternativu z pevné, uzavřené množiny typů a obvykle se zpracovává pomocí std::visit nebo explicitních dotazů na typ. Je užitečný pro zprávy protokolů nebo události, kdy je množina druhů zpráv známá a požadujete typově bezpečné zpracování bez virtuálního volání (virtual dispatch) a často i bez alokace jednotlivých objektů na haldě. Polymorfismus založený na dědičnosti využívá základní třídu a virtuální funkce pro volání přes společné rozhraní; je vhodnější ve chvíli, kdy je množina odvozených typů zpráv otevřená, nezávisle rozšiřitelná, funguje jako pluginy nebo je skryta za stabilním rozhraním. Varianty upřednostňují uzavřené součtové typy (sum types), lokálnost paměti a kontrolu v době překladu; dědičnost naopak upřednostňuje rozšiřitelnost, polymorfismus v době běhu (runtime) a návrh založený na rozhraních.

Zkusit odpovědět na tuto otázku s trenérem AI

Sémantika jazyka

6Co je to nedefinované chování (undefined behavior) v C++ a jak se může projevit při produkčních incidentech v backendu?

Nedefinované chování (UB) je chování, u kterého standard C++ po výskytu neplatné operace neklade žádné požadavky na výsledek. Program může zdánlivě fungovat, spadnout, poškodit data, odhalit bezpečnostní chyby nebo být optimalizován do překvapivého chování. Kompilátory předpokládají, že UB nenastane, a na základě tohoto předpokladu optimalizují kód, takže problémy se mohou objevit až ve finálních sestaveních (release builds) nebo pod produkční zátěží. Incidenty v backendu mohou pocházet z visících ukazatelů/referencí (dangling pointers/references), použití po uvolnění paměti (use-after-free), porušení životnosti (lifetime) objektů, přístupu mimo hranice (out-of-bounds access), přetečení celého čísla se znaménkem (signed integer overflow), souběhu dat (data races), neplatných přetypování, čtení neinicializovaných dat, dvojitého uvolnění paměti (double frees) nebo porušení pravidel striktního aliasingu (strict-aliasing). Zmírnění těchto rizik zahrnuje použití RAII (Resource Acquisition Is Initialization) a jasný návrh vlastnictví (ownership) a životnosti, bezpečnější abstrakce a kontrolu hranic, testování a fuzzing, revize kódu, statickou analýzu a sanitizéry jako ASan, UBSan a TSan.

Zkusit odpovědět na tuto otázku s trenérem AI

7Vysvětlete rozdíl mezi nedefinovaným (undefined), nespecifikovaným (unspecified) a implementačně definovaným (implementation-defined) chováním s uvedením příkladů relevantních pro backend.

Nedefinované chování (undefined behavior - UB) znamená, že standard C++ neklade žádné požadavky; výsledky mohou zahrnovat pády, poškození dat, bezpečnostní chyby nebo chybnou kompilaci v závislosti na optimalizátoru. Mezi příklady patří přístup mimo meze (out-of-bounds access), použití uvolněné paměti (use-after-free), přetečení znaménkového celého čísla a souběh dat (data race). Nespecifikované chování (unspecified behavior) znamená, že standard umožňuje více než jeden výsledek a implementace nemusí dokumentovat, který z nich byl zvolen; běžným příkladem je pořadí vyhodnocování mnoha argumentů funkcí, takže kód by neměl spoléhat na to, že vedlejší efekty budou vyhodnoceny v konkrétním pořadí. Implementačně definované chování (implementation-defined behavior) znamená, že implementace musí chování zvolit a zdokumentovat; příklady zahrnují znaménkovost obyčejného typu `char`, velikosti a rozsahy některých základních typů v rámci limitů standardu a některé chování bitového posunu doprava u znaménkových čísel. V kódu backendu je UB rizikem pro správnost a bezpečnost, zatímco nespecifikované a implementačně definované chování představují rizika pro přenositelnost, kterým je třeba se vyhnout nebo je izolovat v logice protokolů, úložišť a napříč platformami.

Zkusit odpovědět na tuto otázku s trenérem AI

Zpracování chyb

8Vysvětlete úrovně bezpečnosti proti výjimkám (exception safety) a jak `noexcept` ovlivňuje operace přesunu (move operations), kontejnery a generování kódu.

Úrovně bezpečnosti proti výjimkám popisují, co zůstává v platnosti, pokud operace vyhodí výjimku. Základní záruka (basic guarantee) znamená, že jsou zachovány invarianty a nedochází k úniku prostředků, ačkoli stav se mohl změnit. Silná záruka (strong guarantee) znamená sémantiku potvrzení nebo vrácení změn (commit-or-rollback): při selhání zůstává pozorovatelný stav nezměněn. Záruka nevyhození výjimky (nothrow guarantee) znamená, že operace výjimku nevyhodí. Běžné techniky zahrnují RAII (Resource Acquisition Is Initialization), provádění práce na dočasných objektech, idiom copy-and-swap a řazení mutací tak, aby k potvrzení (commit) došlo až po úspěšném dokončení práce, která může vyhodit výjimku. `noexcept` je kontrakt: pokud funkce s `noexcept` vyhodí výjimku, zavolá se `std::terminate`. Ovlivňuje také generický kód a kontejnery: například během realokace může `std::vector` přesouvat prvky, pokud je přesouvací konstruktor (move constructor) označen jako `noexcept`; v opačném případě je může kopírovat, často pomocí `std::move_if_noexcept`, aby se zachovaly záruky bezpečnosti proti výjimkám. `noexcept` může také pomoci při generování kódu nebo optimalizaci tím, že sníží počet potřebných cest pro šíření výjimek, ale pouze tehdy, když je tento slib dodržen.

Zkusit odpovědět na tuto otázku s trenérem AI

9Vysvětlete std::expected nebo ekvivalentní typy pro výsledky (outcome types) jako alternativy k výjimkám pro operace, které mohou selhat (fallible operations).

std::expected<T, E> představuje buď úspěšnou hodnotu T, nebo explicitní chybu E, čímž se selhání stává součástí návratového typu funkce namísto spoléhání se na propagaci výjimek. Je to užitečné pro operace, které mohou selhat, u nichž se chyby očekávají a volající by je měli lokálně zpracovat, jako je parsování, validace, síťová volání, vyhledávání v úložišti a backendové operace, které lze opakovat (retryable). Typ chyby by měl nést strukturované informace, jako je kód chyby, kategorie, možnost opakování, zpráva nebo mapování HTTP/RPC (Hypertext Transfer Protocol / Remote Procedure Call). Typy pro návrat úspěchu či chyby (expected/outcome) lze řetězit kontrolou a propagací chyb a v rozhraních API (Application Programming Interface) ve stylu C++23 mohou využívat monadické operace, jako jsou and_then, transform a or_else, ke spojování jednotlivých kroků bez hluboce zanořených podmínek. Ve srovnání s výjimkami zviditelňují řízení toku (control flow) a kontrakty API, přičemž dobře fungují i přes hranice kódu bez výjimek nebo hranice aplikačního binárního rozhraní ABI (Application Binary Interface); výjimky mohou být stále vhodné pro vzácná, nelokální nebo skutečně výjimečná selhání, v závislosti na pravidlech projektu.

Zkusit odpovědět na tuto otázku s trenérem AI

Životnost objektů

10Popište pravidla životnosti objektů (object lifetime) pro automatické, dynamické, dočasné a asynchronně odkazované objekty.

Automatické objekty existují od své konstrukce až do konce svého oboru platnosti (scope); ukazatele nebo odkazy na ně se po opuštění tohoto oboru stávají neplatnými (dangling). Dynamické objekty existují od alokace/konstrukce až do svého explicitního zničení nebo do okamžiku, kdy je zničí vlastnící objekt; holé ukazatele (raw pointers) a odkazy neprodlužují jejich životnost. Dočasné objekty (temporaries) obvykle existují až do konce celého výrazu, přičemž platí specifická pravidla pro prodloužení životnosti při navázání na odkaz, ale ne pro každé použití. Asynchronní operace, jako jsou zpětná volání (callbacks), vlákna, časovače nebo korutiny, se mohou spustit až po zničení odkazovaného objektu, a proto zachycené proměnné (captures) a uložené odkazy vyžadují explicitní správu životnosti, aby nedošlo ke vzniku neplatných odkazů a k chybám typu použití po uvolnění (use-after-free).

Zkusit odpovědět na tuto otázku s trenérem AI

11Popište problémy s pořadím statické inicializace a vysvětlete, jak je zmírňují `constinit`, takzvané magické statiky (magic statics) a vkládání závislostí (dependency injection).

Problémy s pořadím statické inicializace nastávají kvůli tomu, že dynamicky inicializované objekty v oboru platnosti jmenného prostoru (namespace-scope) nebo statické objekty v různých překladových jednotkách (translation units) mají nespecifikované relativní pořadí inicializace. Jedna globální proměnná může použít jinou ještě předtím, než byla zkonstruována; pořadí destrukce může způsobit podobné problémy při ukončování programu. Klíčové slovo `constinit` vynucuje, aby měla statická proměnná nebo proměnná lokální pro vlákno (thread-local) statickou/konstantní inicializaci (jinak je program nevalidní, tzn. ill-formed), čímž se u dané proměnné předchází závislosti na dynamickém pořadí inicializace. Magické statiky (magic statics), neboli statické proměnné lokální pro funkci, se inicializují při prvním použití a od standardu C++11 jsou bezpečné pro použití z více vláken. Vkládání závislostí (dependency injection) se vyhýbá skrytým globálním závislostem tím, že objekty jsou vytvářeny v kontrolovaném pořadí a závislosti se předávají explicitně.

Zkusit odpovědět na tuto otázku s trenérem AI

12Co je to vynechání kopie (copy elision), NRVO (Named Return Value Optimization) a RVO (Return Value Optimization) a kdy se na ně můžete spolehnout?

Vynechání kopie (copy elision) znamená zkonstruování objektu přímo v jeho konečném umístění namísto vytváření samostatného dočasného objektu a jeho kopírování nebo přesouvání. RVO obvykle odkazuje na vrácení nepojmenovaného dočasného objektu neboli `prvalue`, jako např. `return T{};`; v C++17 a novějších verzích je mnoho z těchto případů povinných, protože `prvalue` inicializuje výsledný objekt přímo. NRVO představuje vrácení pojmenovaného lokálního objektu, jako např. `return x;`. Kompilátor má dovoleno zkonstruovat tento lokální objekt přímo na místě návratové hodnoty volajícího, ale není to zaručeno. Můžete se spolehnout na povinné vynechání `prvalue` z C++17 ve specifikovaných případech, ale nelze vždy spoléhat na to, že dojde k NRVO. Vyhněte se použití `std::move` na pojmenovanou lokální návratovou hodnotu, protože to může aplikaci NRVO zabránit.

Zkusit odpovědět na tuto otázku s trenérem AI

Standardní knihovna

13Jak požadavky na komparátor, rovnost a hašovací funkci ovlivňují správnost asociativních kontejnerů?

Asociativní kontejnery se spoléhají na porovnávání, rovnost a hašování k definování identity klíče a k udržení svých vnitřních invariantů. Uspořádané kontejnery, jako je `std::map`, vyžadují, aby komparátor uplatňoval striktní slabé uspořádání (strict weak ordering); klíče jsou považovány za ekvivalentní, když ani jeden není při porovnání menší než ten druhý, a to nejen nutně pomocí `operator==`. Neuspořádané kontejnery (unordered containers) vyžadují, aby rovnost byla relací ekvivalence, a jakékoli dva klíče považované za stejné musí vyprodukovat stejnou hašovací hodnotu. Klíče nesmí být během uložení měněny způsobem, který by změnil jejich uspořádání, rovnost nebo hašování, protože to může vést k nesprávnému chování při vyhledávání, mazání a zajištění unicity.

Zkusit odpovědět na tuto otázku s trenérem AI

14Vysvětlete heterogenní vyhledávání (heterogeneous lookup) v uspořádaných a neuspořádaných asociativních kontejnerech.

Heterogenní vyhledávání umožňuje prohledávat asociativní kontejner pomocí jiného typu, než je typ jeho klíče, čímž se vyhneme vytváření dočasných klíčů. V uspořádaných kontejnerech to vyžaduje transparentní komparátor, například std::less<> nebo vlastní komparátor se značkou is_transparent. V neuspořádaných kontejnerech to vyžaduje jak transparentní hashovací funktory, tak transparentní funktory pro porovnání na rovnost, které dokážou konzistentně zpracovat typ klíče i typ vyhledávání. Častým příkladem na backendu je kontejner, jehož klíčem je std::string, který lze prohledávat pomocí std::string_view nebo const char* bez alokace dočasného objektu std::string.

Zkusit odpovědět na tuto otázku s trenérem AI

Souběžnost

15Vysvětlete paměťový model v C++: souběhy dat (data races), relaci happens-before a záruky synchronizace.

Paměťový model v C++ definuje, kdy jsou operace v různých vláknech seřazeny a kdy se stávají viditelnými zápisy do paměti. Souběh dat (data race) nastává ve chvíli, kdy dvě vlákna souběžně přistupují ke stejnému místu v paměti, alespoň jeden z těchto přístupů je zápis a tyto přístupy nejsou uspořádány pomocí relace happens-before, ani nejsou jinak zabezpečeny atomickými operacemi; souběh dat způsobuje nedefinované chování (undefined behavior). Happens-before je uspořádací relace, která zaručuje, že dřívější vedlejší účinky (side effects) budou viditelné pro pozdější operace. Toto uspořádání vytvářejí synchronizační operace: například odemknutí mutexu se synchronizuje s (synchronizes-with) pozdějším úspěšným uzamčením stejného mutexu a vhodné atomické operace release/acquire mohou zajistit synchronizaci mezi vlákny. Správně navržené programy používají k vytvoření happens-before vztahů pro sdílený stav mutexy, atomické proměnné nebo jinou synchronizaci.

Zkusit odpovědět na tuto otázku s trenérem AI