Příprava na C++ pohovor

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

Vybrané otázky na pohovor pro C++ backend vývojáře, rozdělené podle témat a generované ze stejného katalogu otázek, který pohání procvičování na EngineerSpeak.

Zahájit C++ backend 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ů.

Resource Management

1Vysvětlete koncept RAII (Resource Acquisition Is Initialization) a jak utváří správu prostředků bezpečnou proti výjimkám v backendových službách v C++.

RAII (Resource Acquisition Is Initialization) znamená, že objekt v C++ vlastní prostředek a uvolňuje jej ve svém destruktoru. Protože se lokální objekty ničí automaticky, když skončí jejich životnost (lifetime) nebo rozsah platnosti (scope), a to i během rozvíjení zásobníku (stack unwinding) při výjimce, poskytuje RAII deterministický úklid a zajišťuje, že zpracování chybových stavů je bezpečné proti výjimkám (exception-safe). V backendových službách se to netýká jen paměti, ale také deskriptorů souborů, soketů, zámků mutexů, databázových spojení (database handles), transakcí a dalších systémových nebo aplikačních prostředků.

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

Správa paměti

2Porovnejte `std::unique_ptr` a `std::shared_ptr` a popište, kdy je který z nich vhodné použít v backendových API.

`std::unique_ptr` představuje výhradní vlastnictví: je levný (bez režie), přesunutelný (movable), ale nikoli kopírovatelný, a je vhodný pro jediného vlastníka nebo pro API (Application Programming Interface), která předávají vlastnictví. `std::shared_ptr` představuje sdílené vlastnictví: je kopírovatelný a udržuje objekt naživu pomocí počítání referencí, dokud jej neuvolní poslední silný vlastník. V backendových API používejte `unique_ptr`, když se předává vlastnictví, `shared_ptr` pouze tehdy, když více nezávislých vlastníků musí prodloužit životnost, a reference nebo surové ukazatele (raw pointers) pro přístup bez vlastnictví. Pro vytváření objektů `shared_ptr` se obvykle upřednostňuje `std::make_shared`, protože je efektivní a bezpečný z hlediska výjimek.

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

3Jak 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

4Vysvě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

Typový systém

5Co je sémantika přesunu (move semantics) a jak implementujete správný konstruktor přesunu a operátor přiřazení přesunem?

Sémantika přesunu umožňuje v C++ převádět prostředky z dočasných nebo jinak postradatelných objektů namísto jejich kopírování. Používá reference na r-hodnoty, jako je `T&&`, a funkci `std::move`, což je ve skutečnosti přetypování, které umožňuje výběr přetížených funkcí pro přesun; samotné `std::move` nic nepřesouvá. Správný konstruktor přesunu (move constructor) inicializuje nový objekt tak, že převezme prostředek zdrojového objektu a zdroj ponechá ve validním, zničitelném a přiřaditelném stavu. Správný operátor přiřazení přesunem (move assignment operator) přesouvá data do existujícího objektu, ošetřuje nebo toleruje přiřazení sobě samému (self-assignment), uvolňuje nebo znovu využívá aktuální prostředek cíle, přebírá zdrojový prostředek a ponechává zdrojový objekt v bezpečném stavu. Operace přesunu by měly být často označeny jako `noexcept`, aby je standardní kontejnery mohly používat při realokaci a zároveň zachovaly záruky ohledně výjimek.

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

6Popište const-korektnost (const-correctness) v C++ API (Application Programming Interface) a jak efektivně navrhovat konstantní členské funkce (`const`).

Const-korektnost znamená vyjádření prostřednictvím typového systému, které operace nemění pozorovatelný nebo logický stav objektu. Členská funkce typu `const` má objekt `this` s kvalifikátorem `const`, takže nemůže měnit datové členy, které nejsou označené jako `mutable`, ani volat členské funkce bez kvalifikátoru `const` na stejném objektu. Správný návrh API označuje dotazy pouze pro čtení jako `const`, vrací hodnoty nebo `const` reference/ukazatele, pokud je to vhodné, a vyhýbá se odhalování měnitelného vnitřního stavu z `const` funkcí. Klíčové slovo `mutable` by mělo být vyhrazeno pro implementační detaily, které nemění logický stav, jako jsou vyrovnávací paměti (caches), líné načítání hodnot (lazy values), metriky nebo mutexy. Klíčové slovo `const` představuje kontrakt API týkající se mutací, nikoli automatickou záruku bezpečnosti pro vlákna (thread safety); záruky souběžnosti vyžadují samostatnou implementaci a dokumentaci.

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

7Co jsou typy umožňující pouze přesun (move-only types) a jak ovlivňují hranice API (Application Programming Interface) pro sockety, obslužné rutiny souborů (file handles) a zámky?

Typy umožňující pouze přesun (move-only types) jsou typy, které nelze kopírovat, ale lze je přesouvat. Běžně se používají pro unikátní vlastnictví prostředků, jako jsou sockety, deskriptory souborů, obslužné rutiny souborů (file handles), zámky a `std::unique_ptr`. Operace kopírování jsou smazány (deleted), aby se zabránilo duplikovanému vlastnictví a dvojitému uvolnění; operace přesunu (move operations) předávají vlastnictví a zanechávají zdroj v platném, ale obvykle prázdném/nevlastnícím stavu. Rozhraní API by měla explicitně vymezovat hranice vlastnictví: továrny (factories) mohou vracet objekty umožňující pouze přesun pomocí hodnoty, funkce přebírající vlastnictví je mohou přijímat hodnotou nebo jako odkaz na r-hodnotu (rvalue reference), a funkce, které objekt pouze zkoumají, by měly přijímat standardní odkazy (references), ukazatele nebo jiné výpůjční mechanismy (borrowing handles). Kontejnery mohou ukládat hodnoty umožňující pouze přesun, pokud jsou do nich prvky vloženy nebo v rámci nich přemístěny přesunem.

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

8Porovnejte `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

Životnost objektů

9Popište Pravidlo nuly (Rule of Zero), Pravidlo tří (Rule of Three) a Pravidlo pěti (Rule of Five) a vysvětlete, kdy se každé z nich uplatňuje.

Pravidlo nuly (Rule of Zero): dávejte přednost třídám, které nedeklarují vlastní destruktor nebo operace kopírování či přesunu; nechte správu prostředků na členech využívajících RAII (Resource Acquisition Is Initialization), jako jsou `std::string`, `std::vector`, `std::unique_ptr`, obálky souborů či soketů a podobně. Pravidlo tří (Rule of Three): pokud třída ručně spravuje prostředek a potřebuje vlastní destruktor, kopírovací konstruktor (copy constructor) nebo operátor přiřazení kopírováním (copy assignment operator), obvykle potřebuje všechny tři, aby správně definovala chování při kopírování a vlastnictví. Pravidlo pěti (Rule of Five): v C++11 a novějších verzích by takové typy měly zvážit také implementaci konstruktoru přesunu a operátoru přiřazení přesunem. Pravidlo nuly používejte pro většinu aplikačních typů; Pravidlo tří/pěti používejte tehdy, když typ přímo vlastní nějaký prostředek nebo má netriviální sémantiku vlastnictví či životnosti.

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

Sémantika jazyka

10Popište formy inicializace v C++ a běžná úskalí, včetně `initializer_list` a agregátů (aggregates).

Jazyk C++ má několik forem inicializace. Výchozí inicializace (default initialization), jako je `T x;`, volá výchozí konstruktor pro typy tříd, ale ponechává automatické proměnné základních typů neinicializované. Inicializace hodnotou (value initialization), jako je `T x{};` nebo `T()`, provede vynulování tam, kde je to vhodné, před samotnou inicializací konstruktorem nebo členskými proměnnými. Inicializace seznamem (list initialization) používá složené závorky, odmítá zužující konverze (narrowing conversions) a má speciální pravidla pro řešení přetížení (overload resolution), včetně silné preference pro vyhovující konstruktory využívající `std::initializer_list`. Inicializace agregátů přímo inicializuje členy agregátu pomocí složených závorek; standard C++20 také podporuje určené inicializátory (designated initializers) pro agregáty, a to v pořadí deklarace. Mezi běžná úskalí patří neinicializované lokální skalární proměnné, překvapivý výběr přetížení pro `initializer_list`, chyby zužujících konverzí při použití složených závorek, problém zvaný most vexing parse při použití kulatých závorek a změny chování, když typ přestane být agregátem.

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

11Co 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

Object Model

12Vysvětlete virtuální funkce, tabulky virtuálních metod (vtables), cenu dynamické vazby (dynamic dispatch), ořezávání objektů (object slicing) a rizika spojená s virtuálními destruktory.

Virtuální funkce umožňuje polymorfismus za běhu: při volání přes ukazatel nebo referenci na základní třídu se vybere implementace odpovídající dynamickému typu objektu. Většina implementací ukládá do každého polymorfního objektu skrytý ukazatel na tabulku virtuálních metod (vptr), který ukazuje na tabulku virtuálních metod (vtable) obsahující adresy virtuálních funkcí pro daný dynamický typ. Typickou cenou je ukazatel navíc v objektu, nepřímé volání, možný dopad na mezipaměť (cache) a predikci větvení (branch prediction) a omezené příležitosti pro vkládání kódu (inlining), ačkoli překladače mohou někdy volání devirtualizovat. K ořezávání objektů (object slicing) dochází, když je odvozený objekt zkopírován nebo uložen hodnotou jako objekt základní třídy, čímž ztratí svou odvozenou část a dynamické chování. Pokud má být základní třída odstraňována přes ukazatel na základní třídu, musí být její destruktor virtuální; v opačném případě má smazání odvozeného objektu přes tento ukazatel nedefinované chování.

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

Standardní knihovna

13Popište hodiny, časové body (time_points) a trvání (durations) v knihovně std::chrono z pohledu použití pro časové limity (timeouts) služeb, metriky a časová razítka (timestamps).

Knihovna std::chrono modeluje čas pomocí hodin (clocks), časových bodů (time_points) a trvání (durations). Trvání je interval s jednotkou, jako jsou milisekundy nebo sekundy. Časový bod je okamžik na časové ose konkrétních hodin. Hodiny steady_clock jsou monotónní a měly by se používat pro uplynulý čas, časové limity služeb, konečné termíny (deadlines) a měření latence, protože nejsou ovlivněny změnami reálného času systému (wall-clock time). Hodiny system_clock reprezentují občanský nebo reálný čas a jsou vhodné pro časová razítka (timestamps), logování, trvalé ukládání (persistence) a převody kalendáře, mohou však poskočit, pokud je upraven systémový čas. Časové limity by měly být obvykle založeny na steady_clock::now() + duration; strojová časová razítka by měla používat jasně zdokumentovaný formát reálného času, typicky UTC (Coordinated Universal Time) na hranicích API (Application Programming Interface) nebo vrstvy pro ukládání dat.

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

14Popište `std::format` a moderní formátovací nástroje ve srovnání s `iostreams` a API (Application Programming Interface) ve stylu `printf`.

`std::format` je typově bezpečný formátovací nástroj ze standardu C++20, inspirovaný knihovnou fmtlib. K vytváření formátovaného textu používá nahrazovací pole `{}` a specifikace formátu bez stavové syntaxe vkládání typické pro `iostreams` nebo C argumentů s proměnnou délkou (varargs) ve stylu `printf`. Ve srovnání s `printf` předchází mnoha problémům s neshodou formátu a typu; oproti `iostreams` je často přehlednější a snáze se skládá. Knihovna fmtlib je široce používaná knihovna, která předcházela `std::format` a ovlivnila jej, přičemž může nabízet širší podporu nebo novější funkce. Uživatelsky definované typy lze formátovat pomocí podpory vlastních formátovačů. U logování s velkým objemem dat závisí výkon na tom, jak se vyhnout zbytečnému formátování, konverzím a alokacím, a to zejména pro vypnuté úrovně logování; preferována jsou API, která formátování odkládají nebo nejprve kontrolují úroveň logování.

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

Šablony

15Vysvě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

Souběžnost

16Popište prevenci uváznutí (deadlock) a strategie uzamykání více mutexů pomocí std::scoped_lock a std::lock.

Uváznutí (deadlock) se předchází vyhýbáním se cyklickému čekání: pokud je to možné, získávejte zámky v konzistentním globálním pořadí, nebo získejte více mutexů pomocí `std::lock`/`std::scoped_lock`, které používají algoritmus pro zamezení uváznutí. `std::scoped_lock lock(a, b, ...)` je nejjednodušší forma RAII (Resource Acquisition Is Initialization) pro uzamčení několika mutexů a jejich automatické odemčení při opuštění oboru platnosti (scope). S `std::lock` nejprve uzamkněte mutexy, poté připojte RAII obálky pomocí `std::adopt_lock`, nebo použijte `std::unique_lock` s `std::defer_lock`. Udržujte kritické sekce krátké a vyhněte se blokujícím operacím nebo neznámým zpětným voláním (callbacks) během držení zámků; zamezení uváznutí automaticky nezaručuje spravedlnost (fairness) a nezabrání všem scénářům typu livelock (živé uváznutí) nebo vyhladovění (starvation).

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

17Vysvětlete vzory použití `condition_variable` pro čekání na změny stavu.

Použijte `std::condition_variable` k čekání na změnu stavového predikátu chráněného mutexem. Sdílený stav je upravován, zatímco je držen stejný mutex, poté `notify_one` nebo `notify_all` probudí čekající vlákna. Čekající vlákna by měla používat `cv.wait(lock, predicate)` nebo ekvivalentní smyčku, protože probuzení mohou být falešná (spurious wakeups) a oznámení nejsou uchovávána nezávisle na stavu predikátu. `notify_one` probudí jedno čekající vlákno; `notify_all` probudí všechna čekající vlákna a je vhodné pro hromadné (broadcast) změny stavu, jako je ukončení aplikace (shutdown).

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

18Navrhněte vláknově bezpečnou omezenou frontu (bounded queue) pomocí standardních primitiv jazyka C++ a definujte záruky jejího API.

Omezenou vláknově bezpečnou frontu lze vytvořit pomocí `std::mutex`, podmíněných proměnných (condition variables), kontejneru s pevnou kapacitou a příznaku ukončení/zavření. Operace `push` se zablokuje, vyprší její časový limit nebo selže, když je fronta plná, což poskytuje zpětný tlak (backpressure); `pop` se zablokuje, když je prázdná. Obě operace by měly čekat na predikáty jako `size < capacity || closed` a `!empty || closed`. Při ukončení/zavření probuďte zablokované producenty a konzumenty; odmítněte nová vkládání a definujte, zda konzumenti vyprázdní existující položky, nebo se okamžitě zastaví. API by mělo specifikovat chování při blokování, sémantiku zavření, návratové hodnoty a záruky souběžnosti.

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

Database

19Jak provedete migraci schématu pro službu v C++ bez výpadku (downtime)?

Použijte fázovanou migraci typu expand-contract. Nejprve proveďte zpětně kompatibilní rozšíření schématu, jako jsou sloupce povolující hodnotu null (nullable) nebo nové tabulky, které nerozbijí aktuálně běžící službu v C++. Nasaďte kompatibilní kód, který dokáže pracovat jak se starou, tak s novou reprezentací, zpětně doplňte existující data (backfill) v malých regulovaných dávkách, ověřte konzistenci, přepněte čtení na nové schéma a teprve později odstraňte staré sloupce nebo kód, jakmile na nich žádné nasazené verze nebudou záviset. U velkých tabulek se vyhněte dlouhým blokujícím DDL (Data Definition Language) operacím, použijte online/souběžné operace tam, kde jsou podporovány, monitorujte zámky, replikaci a prostředky a udržujte cesty pro návrat zpět (rollback), které fungují s částečně nasazeným kódem a částečně migrovanými daty.

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

20Jak by měla služba v C++ číst z databázových replik a zároveň spravovat zastaralá čtení (stale reads) a záruky čtení vlastních zápisů (read-your-writes)?

Přistupujte ke čtení z replik jako k explicitnímu kompromisu v rámci konzistence (consistency trade-off). Čtení, která snesou zastaralost dat (staleness), mohou směřovat na zdravé repliky, ale čtení s požadavkem na 'read-your-writes', transakční čtení nebo čtení kritická z hlediska čerstvosti dat (freshness) by měla směřovat na primární databázi, pokud není známo, že vybraná replika již přehrála příslušnou pozici zápisu, jako je LSN (Log Sequence Number), časové razítko nebo verze. Sledujte zpoždění (lag) a zdraví replik, zakódujte požadovanou konzistenci pro každý koncový bod nebo požadavek a v případě, že repliky překročí povolenou míru zastaralosti dat, přejděte na primární databázi (fallback), počkejte na dohnání zpoždění (catch-up), nebo vraťte chybu. Dokumentujte model konzistence, aby volající věděli, která čtení mohou být zastaralá.

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