Forberedelse til Middle C++

Interviewspørgsmål til Middle C++ Backend-udviklere

15 udvalgte interviewspørgsmål til Middle C++ backend-udviklere, der skal kunne drøfte ydeevne, ejerskab og produktionsafvejninger.

Start et AI-baseret Middle C++ interviewIntet kreditkort kræves. 1 gratis session er tilgængelig.
Øv tekniske jobsamtaler på engelskEn tilstand, hvor ikke-modersmålstalende kan øve sig i at bestå tekniske interviews.

Hukommelseshåndtering

1Hvordan fungerer brugerdefinerede deleters i smart pointers, og hvornår er de nyttige for C/C++ interoperabilitet?

En brugerdefineret deleter er kaldbart oprydningslogik, som bruges af en smart pointer i stedet for standard delete-operationen, når pointeren frigiver sin ejede ressource. Det er nyttigt for C/C++ interoperabilitet, når en ressource skal frigives med en specifik funktion som fclose, close, curl_easy_cleanup, SSL_free, free eller en biblioteks-nedrivningsfunktion. For unique_ptr er deleter-typen en del af unique_ptr-typen og kan påvirke dens størrelse; tilstandsløse deleters kan optimeres væk, mens funktionspointere eller tilstandsfyldte deleters tilføjer lagerplads. For shared_ptr lagres deleteren i kontrolblokken og køres, når den sidste stærke ejer frigiver objektet. Brugerdefinerede deleters lader C-ressourcer indgå sikkert i RAII (Resource Acquisition Is Initialization).

Prøv at besvare dette spørgsmål med en AI-coach

2Forklar kontrolblok-mekanikken bag `weak_ptr` og `shared_ptr`, herunder cyklusser, `enable_shared_from_this` og omkostninger ved referenceoptælling.

En `shared_ptr` håndterer delt ejerskab via en kontrolblok, der indeholder stærke og svage referenceoptællinger samt oprydningsinformation såsom slettefunktionen og allokatoren. Kopiering eller destruktion af `shared_ptr`-objekter tæller den stærke reference op eller ned, typisk med atomare operationer, så separate `shared_ptr`-objekter kan manipuleres sikkert på tværs af tråde, men hver opdatering af referenceoptællingen har en omkostning. Når den stærke optælling når nul, destrueres det administrerede objekt; kontrolblokken forbliver, indtil de svage referencer også er væk. En `weak_ptr` peger på den samme kontrolblok uden at forlænge objektets levetid; `lock()` returnerer en `shared_ptr`, hvis objektet stadig er i live, og ellers en tom `shared_ptr`. Cyklusser, der kun består af `shared_ptr`, forårsager hukommelseslækager, fordi de stærke optællinger aldrig når nul, så `weak_ptr` bruges til bagud-pointere eller observer-links. `enable_shared_from_this` lader et objekt, der allerede ejes af en `shared_ptr`, oprette en ny `shared_ptr` til sig selv ved hjælp af den eksisterende kontrolblok, hvilket undgår farlige, separate kontrolblokke. Omkostninger ved referenceoptælling inkluderer atomare op-/nedtællinger, cachekonflikter, allokering af kontrolblokke samt overhead i kodeområder, der er kritiske for ydeevnen.

Prøv at besvare dette spørgsmål med en AI-coach

Templates

3Forklar værdikategorier ("value categories") og "perfect forwarding" i C++, og hvorfor de er vigtige for effektive generiske API'er.

Værdikategorier i C++ beskriver udtryk: lvalues har en identitet og kan refereres til efter udtrykket; prvalues er rene rvalues som f.eks. mange midlertidige eller beregnede værdier; xvalues er udløbende objekter ("expiring objects"), hvis ressourcer kan genbruges. "Perfect forwarding" er en template-teknik, hvor man tager en forwarding-reference, typisk `T&&` hvor `T` udledes, og videresender med `std::forward<T>(arg)`, så kalderens værdikategori bevares: lvalues forbliver lvalues, og rvalues forbliver rvalues. Reglerne for "reference collapsing" gør dette muligt. Det har betydning for generiske backend-API'er (Application Programming Interface), fordi wrappers, fabrikker ("factories"), dispatchere og emplace-lignende funktioner kan undgå unødvendige kopier og samtidig bevare valg af overload samt move-adfærd.

Prøv at besvare dette spørgsmål med en AI-coach

Typesystem

4Sammenlign `auto`, `decltype`, `decltype(auto)` og udledning af skabelonargumenter (template argument deduction) i typisk backend-kode.

`auto` bruger skabelonlignende udledning (template deduction) til variabler: Simpel `auto` kasserer normalt referencer og `const` på topniveau, medmindre deklarationen beder om dem, såsom `auto&`, `const auto&` eller `auto&&`. `decltype(expr)` undersøger den deklarerede type eller udtrykkets type mere præcist: Et id-udtryk uden parenteser giver den deklarerede type, mens andre lvalue-udtryk producerer `T&`, xvalues producerer `T&&`, og prvalues producerer `T`. `decltype(auto)` udleder typen ved hjælp af `decltype`-reglerne, hvilket ofte bruges til returtyper, hvor referencer skal bevares. Udledning af skabelonargumenter ligner `auto`, men afhænger af parameterformen, som f.eks. `T`, `T&`, `const T&` eller `T&&`, og har sine egne regler. Initialisering med tuborgklammer er en typisk forskel: `auto x = {1,2}` udleder `std::initializer_list<int>`, hvorimod en almindelig skabelonparameter generelt ikke kan udlede `T` fra en ren initialisering med tuborgklammer, medmindre parameteren forventer en `initializer_list` eller en anden passende type.

Prøv at besvare dette spørgsmål med en AI-coach

5Sammenlign `std::variant` med nedarvningsbaseret polymorfi til modellering af heterogene beskeder eller hændelser.

`std::variant` er en værditype, der indeholder nøjagtigt ét alternativ fra en fast, lukket mængde af typer og håndteres typisk med `std::visit` eller eksplicitte typeforespørgsler. Den er nyttig til protokolbeskeder eller hændelser, når mængden af beskedtyper er kendt, og man ønsker typesikker håndtering uden virtuel dispatch og ofte uden heap-allokering per objekt. Nedarvningsbaseret polymorfi bruger en basisklasse og virtuelle funktioner til at dispatche gennem en fælles grænseflade; det er bedre, når mængden af nedarvede beskedtyper er åben, uafhængigt udvidelig, fungerer som plugins eller er skjult bag en stabil grænseflade. `std::variant` favoriserer lukkede sumtyper, lokalitet og kompileringstids-kontrol; nedarvning favoriserer udvidelighed, polymorfi ved kørselstid og grænsefladebaseret design.

Prøv at besvare dette spørgsmål med en AI-coach

Sprogsemantik

6Hvad er udefineret adfærd (undefined behavior) i C++, og hvordan kan det komme til udtryk i backend-hændelser i produktion?

Udefineret adfærd er adfærd, for hvilken C++-standarden ikke stiller nogen krav, efter at en ugyldig operation har fundet sted. Programmet kan tilsyneladende fungere, crashe, korrumpere data, afsløre sikkerhedsfejl eller blive optimeret til overraskende adfærd. Compilere antager, at UB (Undefined Behavior) ikke forekommer, og de optimerer baseret på denne antagelse, så problemer dukker muligvis kun op i release-builds eller under produktionstrafik. Backend-hændelser kan stamme fra hængende pointere/referencer (dangling pointers/references), brug-efter-frigivelse (use-after-free), overtrædelser af objekters levetid, tilgang uden for grænserne (out-of-bounds access), overflow af fortegnede heltal, dataræs (data races), ugyldige typekonverteringer (invalid casts), læsning af uinitialiserede data, dobbeltfrigivelse (double frees) eller overtrædelser af strict-aliasing. Mitigering omfatter RAII (Resource Acquisition Is Initialization) og et klart design af ejerskab og levetid, sikrere abstraktioner og grænsekontrol, test og fuzzing, kodegennemgang, statisk analyse samt sanitizers som ASan, UBSan og TSan.

Prøv at besvare dette spørgsmål med en AI-coach

7Gør rede for forskellene mellem udefineret (undefined), uspecificeret (unspecified) og implementeringsdefineret (implementation-defined) adfærd med backend-relevante eksempler.

Udefineret adfærd (undefined behavior, UB) betyder, at C++-standarden ikke stiller nogen krav; resultaterne kan omfatte nedbrud, datakorruption, sikkerhedsfejl eller optimeringsafhængig fejlkompilering. Eksempler inkluderer adgang uden for grænserne, use-after-free, overløb af heltal med fortegn (signed integer overflow) og dataræs (data races). Uspecificeret adfærd betyder, at standarden tillader mere end ét resultat, og implementeringen ikke behøver at dokumentere, hvilket der vælges. Et almindeligt eksempel er evalueringsrækkefølgen af mange funktionsargumenter, så koden bør ikke afhænge af, at bivirkninger evalueres i en bestemt rækkefølge. Implementeringsdefineret adfærd betyder, at implementeringen skal vælge og dokumentere adfærden; eksempler inkluderer, om en almindelig `char` har fortegn eller ej, størrelser/intervaller for visse grundlæggende typer inden for standardens grænser, og visse aspekter af højreforskydning (right shift) med fortegn. I backend-kode er UB en risiko for korrekthed og sikkerhed, mens uspecificeret og implementeringsdefineret adfærd er portabilitetsrisici, der bør undgås eller isoleres i protokol-, lagrings- og platformsuafhængig logik.

Prøv at besvare dette spørgsmål med en AI-coach

Fejlhåndtering

8Forklar niveauer for undtagelsessikkerhed (exception safety), og hvordan `noexcept` påvirker flytteoperationer, beholdere og kodegenerering.

Niveauer for undtagelsessikkerhed beskriver, hvad der forbliver sandt, hvis en operation kaster en undtagelse. Den grundlæggende garanti (basic guarantee) betyder, at invarianter bevares, og ressourcer ikke lækkes, selvom tilstanden kan have ændret sig. Den stærke garanti (strong guarantee) betyder commit-eller-rollback-semantik: ved fejl forbliver den observerbare tilstand uændret. Nothrow-garantien (nothrow guarantee) betyder, at operationen ikke kaster undtagelser. Almindelige teknikker omfatter RAII (Resource Acquisition Is Initialization), udførelse af arbejde på midlertidige objekter, kopier-og-byt (copy-and-swap), samt at ordne ændringer, så bekræftelsen (commit) først sker, når arbejde, der kan kaste undtagelser, er fuldført. `noexcept` er en kontrakt: hvis en `noexcept`-funktion kaster en undtagelse, kaldes `std::terminate`. Det påvirker også generisk kode og beholdere: for eksempel kan `std::vector` under genallokering flytte elementer, når flytte-konstruktøren (move constructor) er `noexcept`; ellers kopierer den muligvis, ofte via `std::move_if_noexcept`, for at bevare undtagelsesgarantier. `noexcept` kan også hjælpe med kodegenerering eller optimering ved at reducere de nødvendige stier til propagering af undtagelser, men kun når løftet er korrekt.

Prøv at besvare dette spørgsmål med en AI-coach

9Forklar `std::expected` eller tilsvarende resultatyper som alternativer til undtagelser (exceptions) for fejlbarlige operationer.

`std::expected<T, E>` repræsenterer enten en succesfuld værdi `T` eller en eksplicit fejl `E`, hvilket gør fejl til en del af funktionens returtype i stedet for at bero på udbredelse af undtagelser. Det er nyttigt for fejlbarlige operationer, hvor fejl forventes, og hvor kalderen bør håndtere dem lokalt, f.eks. ved parsing, validering, netværkskald, opslag i lagre og backend-operationer, der kan forsøges igen. Fejltypen bør bære struktureret information såsom en fejlkode, kategori, om den kan forsøges igen, en besked eller HTTP/RPC-mapping. Typer som `expected` eller `outcome` kan sammensættes ved at kontrollere og videregive fejl, og i C++23-lignende API'er kan man bruge monadiske operationer som `and_then`, `transform` og `or_else` til at sammenkæde trin uden dybt indlejrede betingelser. Sammenlignet med undtagelser gør de kontrolstrømme og API-kontrakter eksplicitte og fungerer godt på tværs af undtagelsesfrie grænser eller ABI-grænser; undtagelser kan stadig være passende for sjældne, ikke-lokale eller sandt ekstraordinære fejl afhængigt af projektets politik.

Prøv at besvare dette spørgsmål med en AI-coach

Objekters levetid

10Beskriv reglerne for objekters levetid for automatiske, dynamiske, midlertidige og asynkront refererede objekter.

Automatiske objekter lever fra de bliver konstrueret, indtil slutningen af deres gyldighedsområde (scope); pointere eller referencer til dem bliver hængende (dangling), når dette område forlades. Dynamiske objekter lever fra allokering/konstruktion, indtil de destrueres eksplicit, eller et ejende objekt destruerer dem; rå pointere og referencer forlænger ikke levetiden. Midlertidige objekter (temporaries) lever normalt indtil slutningen af det fulde udtryk, med specifikke regler for forlængelse af levetiden, når de bindes til referencer, men dog ikke ved enhver anvendelse. Asynkront arbejde såsom callbacks, tråde, timere eller coroutines kan køre, efter at det refererede objekt er blevet destrueret, så indfangede variabler (captures) og gemte referencer kræver eksplicit håndtering af levetid for at undgå hængende referencer og brug efter frigivelse (use-after-free).

Prøv at besvare dette spørgsmål med en AI-coach

11Beskriv problemer med statisk initialiseringsrækkefølge (static initialization order issues), og hvordan `constinit`, "magic statics" og afhængighedsinjektion (dependency injection) afbøder dem.

Problemer med statisk initialiseringsrækkefølge opstår, fordi dynamisk initialiserede objekter på navnerumsniveau (namespace-scope) eller statiske objekter i forskellige oversættelsesenheder (translation units) har en uspecificeret relativ initialiseringsrækkefølge. Én global variabel kan finde på at bruge en anden, før den er blevet konstrueret; destruktionsrækkefølgen kan også skabe lignende problemer ved nedlukning af programmet. `constinit` gennemtvinger, at en statisk eller trådlokal (thread-local) variabel har statisk eller konstant initialisering, ellers er programmet ugyldigt (ill-formed). Dette forhindrer, at den pågældende variabel bliver afhængig af dynamisk initialiseringsrækkefølge. "Magic statics", eller funktionslokale statiske variabler, initialiseres ved første brug og har været trådsikre siden C++11. Afhængighedsinjektion undgår skjulte globale afhængigheder ved at konstruere objekter i en kontrolleret rækkefølge og overføre afhængighederne eksplicit.

Prøv at besvare dette spørgsmål med en AI-coach

12Hvad er copy elision, NRVO (Named Return Value Optimization) og RVO (Return Value Optimization), og hvornår kan du stole på dem?

Copy elision betyder, at et objekt konstrueres direkte på sin endelige destination i stedet for at oprette et separat midlertidigt objekt, der derefter kopieres eller flyttes. RVO refererer normalt til returnering af et unavngivet midlertidigt objekt eller en `prvalue`, såsom `return T{};`. I C++17 og nyere er mange af disse tilfælde obligatoriske, fordi en `prvalue` initialiserer resultatobjektet direkte. NRVO er returnering af et navngivet lokalt objekt, såsom `return x;`. Her har compileren tilladelse til at konstruere det lokale objekt direkte i kalderens returplads (return slot), men dette er ikke garanteret. Du kan stole på obligatorisk `prvalue`-elision i C++17 i de angivne tilfælde, men ikke på, at NRVO altid sker. Undgå at bruge `std::move` på en navngivet lokal returværdi, da det kan forhindre NRVO.

Prøv at besvare dette spørgsmål med en AI-coach

Standardbiblioteket

13Hvordan påvirker krav til sammenligningsfunktioner, lighed og hashfunktioner korrektheden af associative containere?

Associative containere er afhængige af sammenligning, lighed og hashing for at definere nøglers identitet og for at opretholde deres interne invarianter. Sorterede containere som `std::map` kræver, at sammenligningsfunktionen pålægger en streng svag ordning (strict weak ordering); nøgler betragtes som ækvivalente, når ingen af dem er mindre end den anden ved sammenligning, og ikke nødvendigvis ved brug af `operator==`. Usorterede containere kræver, at lighed udgør en ækvivalensrelation, og to vilkårlige nøgler, der betragtes som ens, skal producere den samme hashværdi. Nøgler må ikke ændres, mens de er lagret, på en måde der påvirker deres ordning, lighed eller hash, fordi dette kan gøre adfærden for opslag, sletning og unikhed ukorrekt.

Prøv at besvare dette spørgsmål med en AI-coach

14Forklar heterogent opslag (heterogeneous lookup) i ordnede og uordnede associative containere.

Heterogent opslag gør det muligt at søge i en associativ container med en anden type end dens nøgletype, hvilket undgår oprettelsen af midlertidige nøgler. I ordnede containere kræver dette en transparent sammenligningsfunktion, for eksempel std::less<> eller en brugerdefineret sammenligningsfunktion med en is_transparent-markør. I uordnede containere kræver det både transparente hash- og ligheds-funktorer, der konsekvent kan håndtere nøgletypen og opslagstypen. Et almindeligt backend-eksempel er en container med std::string som nøgle, der kan gennemsøges med std::string_view eller const char* uden at allokere en midlertidig std::string.

Prøv at besvare dette spørgsmål med en AI-coach

Samtidighed

15Forklar C++-hukommelsesmodellen (memory model): dataræs (data races), happens-before og synkroniseringsgarantier.

C++-hukommelsesmodellen definerer, hvornår operationer i forskellige tråde bliver ordnet, og hvornår skrivninger bliver synlige. Et dataræs opstår, når to tråde tilgår den samme hukommelsesplacering samtidigt, mindst én af tilgangene er en skrivning, og tilgangene ikke er ordnet via happens-before eller på anden vis gjort sikre gennem atomiske operationer; et dataræs forårsager udefineret adfærd (undefined behavior). Happens-before er den ordningsrelation, der gør tidligere sideeffekter synlige for senere operationer. Synkroniseringsoperationer skaber denne ordning: For eksempel vil en oplåsning af en mutex synkronisere med (synchronizes-with) en senere, vellykket låsning af den samme mutex, og passende atomiske release/acquire-operationer kan synkronisere mellem tråde. Korrekte programmer bruger mutexes, atomics eller anden synkronisering til at etablere happens-before for delt tilstand.

Prøv at besvare dette spørgsmål med en AI-coach