Geselecteerde C++ interviewvragen voor backenddevelopers, gegroepeerd op onderwerp en afkomstig uit dezelfde catalogus die de oefensessies van EngineerSpeak aandrijft.
1Leg uit wat RAII (Resource Acquisition Is Initialization) is en hoe het resourcebeheer veilig ten opzichte van uitzonderingen maakt in C++ backend-services.
RAII houdt in dat een C++-object eigenaar is van een resource en deze weer vrijgeeft in zijn destructor. Omdat lokale objecten automatisch worden vernietigd wanneer hun levensduur of bereik eindigt, inclusief tijdens het afwikkelen van de stack bij een uitzondering, zorgt RAII voor deterministisch opruimen en maakt het foutpaden veilig ten opzichte van uitzonderingen. In backend-services is dit niet alleen van toepassing op geheugen, maar ook op bestandsdescriptoren, sockets, mutex-locks, database-handles, transacties en andere besturingssysteem- of applicatieresources.
2Vergelijk `std::unique_ptr` en `std::shared_ptr` en beschrijf wanneer elk geschikt is in backend-API's.
`std::unique_ptr` vertegenwoordigt exclusief eigendom: het is goedkoop, verplaatsbaar maar niet kopieerbaar, en is geschikt voor een enkele eigenaar of voor API's die eigendom overdragen. `std::shared_ptr` vertegenwoordigt gedeeld eigendom: het is kopieerbaar en houdt een object in leven door middel van referentietelling totdat de laatste sterke eigenaar het vrijgeeft. Gebruik in backend-API's `unique_ptr` wanneer eigendom wordt overgedragen, `shared_ptr` alleen wanneer meerdere onafhankelijke eigenaren de levensduur moeten verlengen, en referenties of ruwe pointers voor toegang zonder eigenaarschap. Het gebruik van `std::make_shared` heeft doorgaans de voorkeur bij het maken van `shared_ptr`-objecten omdat het efficiënt is en veilig ten opzichte van uitzonderingen.
3Hoe werken custom deleters in smart pointers en wanneer zijn ze nuttig voor interoperabiliteit tussen C en C++?
Een custom deleter is aanroepbare opruimlogica die door een smart pointer wordt gebruikt in plaats van de standaard delete-operatie, wanneer de pointer de resource vrijgeeft die hij in beheer heeft. Dit is nuttig voor interoperabiliteit tussen C en C++ wanneer een resource moet worden vrijgegeven met een specifieke functie, zoals fclose, close, curl_easy_cleanup, SSL_free, free of een vernietigingsfunctie van een bibliotheek. Voor unique_ptr maakt het type van de deleter deel uit van het unique_ptr-type en kan het de grootte ervan beïnvloeden; statusloze deleters kunnen worden weggeoptimaliseerd, terwijl functiepointers of deleters met een status extra opslagruimte in beslag nemen. Voor shared_ptr wordt de deleter opgeslagen in het controleblok en uitgevoerd wanneer de laatste sterke eigenaar het object vrijgeeft. Custom deleters zorgen ervoor dat C-resources veilig kunnen deelnemen aan RAII (Resource Acquisition Is Initialization).
4Leg de werking van het controleblok voor `weak_ptr` en `shared_ptr` uit, inclusief cycli, `enable_shared_from_this` en de prestatiekosten van referentietellers.
Een `shared_ptr` beheert gedeeld eigenaarschap via een controleblok dat een sterke (strong) en zwakke (weak) referentieteller bevat, plus opschooninformatie zoals de 'deleter' en 'allocator'. Het kopiëren of vernietigen van `shared_ptr`-objecten verhoogt of verlaagt de sterke teller. Dit gebeurt doorgaans met atomaire bewerkingen, zodat afzonderlijke `shared_ptr`-objecten veilig verdeeld over meerdere threads kunnen worden gebruikt, maar elke bijwerking van de teller brengt kosten met zich mee. Wanneer de sterke teller nul bereikt, wordt het beheerde object vernietigd; het controleblok blijft bestaan totdat ook alle zwakke referenties verdwenen zijn. Een `weak_ptr` verwijst naar hetzelfde controleblok zonder de levensduur van het object te verlengen; de methode `lock()` retourneert een `shared_ptr` als het object nog leeft, en anders een lege `shared_ptr`. Cycli die uitsluitend uit `shared_ptr`-objecten bestaan veroorzaken geheugenlekken omdat de sterke tellers nooit op nul uitkomen, en daarom wordt `weak_ptr` veelgebruikt voor terugverwijzingen (back-pointers) of links voor waarnemers. Via `enable_shared_from_this` kan een object dat al beheerd wordt door een `shared_ptr`, een nieuwe `shared_ptr` naar zichzelf aanmaken op basis van het bestaande controleblok, waardoor gevaarlijke dubbele controleblokken voor hetzelfde object worden vermeden. De kosten van referentietellers bestaan onder meer uit atomaire verhogingen/verlagingen, cache-contention, geheugentoewijzing voor het controleblok en overhead in vaak uitgevoerde codepaden.
5Wat is move-semantiek en hoe implementeer je een correcte move-constructor en move-toewijzingsoperator?
Move-semantiek stelt C++ in staat om resources over te dragen van tijdelijke of anderszins vervangbare objecten in plaats van ze te kopiëren. Het maakt gebruik van rvalue-referenties zoals `T&&` en `std::move`, wat een cast is die het mogelijk maakt om move-overloads te selecteren; `std::move` zelf verplaatst niets. Een correcte move-constructor initialiseert een nieuw object door de resource van het bronobject over te nemen, en laat de bron achter in een toestand die geldig is en veilig vernietigd of toegewezen kan worden. Een correcte move-toewijzingsoperator verplaatst resources naar een bestaand object, handelt zelftoewijzing correct af, geeft de huidige resource van het doel vrij of hergebruikt deze, neemt de resource van de bron over en laat de bron in een veilige staat achter. Move-operaties moeten vaak `noexcept` zijn, zodat standaardcontainers deze kunnen gebruiken tijdens herallocaties, met behoud van de garanties bij uitzonderingen.
6Beschrijf const-correctness in C++ API's en hoe je effectief const-memberfuncties ontwerpt.
Const-correctness betekent dat je via het typesysteem uitdrukt welke bewerkingen de waarneembare of logische toestand van een object niet wijzigen. Een `const`-memberfunctie heeft een const-gekwalificeerd `this`-object, dus het kan geen niet-veranderlijke datamembers wijzigen of niet-`const` memberfuncties op hetzelfde object aanroepen. Een goed API-ontwerp markeert alleen-lezen query's als `const`, retourneert waarden of `const`-referenties/pointers wanneer dat gepast is, en vermijdt het blootstellen van een veranderlijke interne toestand vanuit `const`-functies. Het sleutelwoord `mutable` moet gereserveerd worden voor implementatiedetails die de logische toestand niet veranderen, zoals caches, lazy waarden, metrieken of mutexen. `const` is een API-contract over mutatie, geen automatische garantie voor threadveiligheid; garanties voor gelijktijdigheid vereisen een afzonderlijke implementatie en documentatie.
7Wat zijn move-only typen en hoe beïnvloeden ze API-grenzen voor sockets, bestands-handles en vergrendelingen?
Move-only typen zijn typen die niet gekopieerd, maar wel verplaatst kunnen worden. Ze komen vaak voor bij uniek eigenaarschap van resources zoals sockets, bestandsdescriptoren, bestands-handles, vergrendelingen en `std::unique_ptr`. Kopieeroperaties zijn verwijderd om dubbel eigenaarschap en dubbele vrijgave te voorkomen; verplaatsingsoperaties dragen het eigenaarschap over en laten de bron geldig maar meestal leeg of zonder eigenaarschap achter. API's moeten eigenaarschapgrenzen expliciet maken: factory-functies kunnen move-only objecten als waarde retourneren, functies die eigenaarschap overnemen kunnen ze als waarde of als rvalue-referentie accepteren, en functies die alleen inspecteren horen referenties, pointers of andere referentietypen zonder eigenaarschap te accepteren. Containers kunnen move-only waarden opslaan wanneer elementen worden ingevoegd of verplaatst via een move-operatie.
8Vergelijk `auto`, `decltype`, `decltype(auto)` en template-argumentdeductie in veelvoorkomende backend-code.
`auto` gebruikt op templates lijkende deductie voor variabelen: een gewone `auto` laat normaal gesproken referenties en top-level `const` weg, tenzij de declaratie hierom vraagt, zoals bij `auto&`, `const auto&` of `auto&&`. `decltype(expr)` inspecteert het gedeclareerde type of expressietype nauwkeuriger: een id-expressie zonder haakjes geeft het gedeclareerde type, terwijl andere lvalue-expressies `T&` opleveren, xvalues `T&&` opleveren en prvalues `T` opleveren. `decltype(auto)` leidt af met behulp van `decltype`-regels, vaak voor retourtypen wanneer referenties behouden moeten blijven. Template-argumentdeductie is vergelijkbaar met `auto`, maar is afhankelijk van de vorm van de parameter, zoals `T`, `T&`, `const T&` of `T&&`, en heeft zijn eigen regels. Initialisatie met accolades (braced initializers) is een veelvoorkomend verschil: `auto x = {1,2}` leidt `std::initializer_list<int>` af, terwijl een gewone template-parameter over het algemeen `T` niet kan afleiden uit een kale initialisatie met accolades, tenzij de parameter een `initializer_list` of een ander geschikt type verwacht.
9Beschrijf de Rule of Zero, Rule of Three en Rule of Five, en geef aan wanneer elke regel van toepassing is.
Rule of Zero: geef de voorkeur aan klassen die geen aangepaste destructor-, kopieer- of verplaatsingsoperaties (move) declareren; laat RAII (Resource Acquisition Is Initialization)-leden zoals `std::string`, `std::vector`, `std::unique_ptr`, bestands- of socket-wrappers, enzovoort, de bronnen beheren. Rule of Three: als een klasse handmatig een bron beheert en een aangepaste destructor, kopieerconstructor of kopieertoewijzingsoperator (copy assignment) nodig heeft, zijn doorgaans alle drie nodig om het juiste kopieer- en eigenaarschapgedrag te definiëren. Rule of Five: in C++11 en later moeten dergelijke typen ook een move-constructor en move-toewijzingsoperator overwegen. Gebruik de Rule of Zero voor de meeste typen in een applicatie; gebruik de Rule of Three/Five wanneer het type direct eigenaar is van een bron of niet-triviale semantiek voor eigenaarschap of levensduur heeft.
10Beschrijf de verschillende vormen van initialisatie in C++ en veelvoorkomende valkuilen, waaronder initializer_list en aggregaten.
C++ kent verschillende vormen van initialisatie. Standaardinitialisatie (default initialization), zoals `T x;`, roept een standaardconstructor aan voor klassetypen, maar laat automatische basistypen ongeïnitialiseerd. Waarde-initialisatie (value initialization), zoals `T x{};` of `T()`, voert waar mogelijk *zero-initialization* uit, nog vóór de initiatie door een constructor of van leden. Lijstinitialisatie (list initialization) gebruikt accolades, weigert zogenaamde *narrowing conversions* (waarbij precisie verloren gaat), en kent speciale regels voor *overload resolution*. Dit houdt onder meer een sterke voorkeur in voor toepasbare `std::initializer_list`-constructors. Aggregaat-initialisatie stelt direct de leden van een aggregaat in via accolades; C++20 ondersteunt daarnaast ook *designated initializers* voor aggregaten in de volgorde van declaratie. Veelvoorkomende valkuilen zijn ongeïnitialiseerde lokale scalaire variabelen, een onverwachte selectie van `initializer_list`-overloads, *narrowing*-fouten met accolades, de zogenoemde *most vexing parse* met haakjes, en onverwachte veranderingen in gedrag wanneer een type niet langer als een aggregaat wordt beschouwd.
11Wat is ongedefinieerd gedrag (undefined behavior) in C++, en hoe kan dit optreden bij productie-incidenten in een backend?
Ongedefinieerd gedrag is gedrag waarvoor de C++-standaard geen vereisten oplegt nadat een ongeldige bewerking is opgetreden. Het programma kan lijken te werken, crashen, data beschadigen, beveiligingslekken blootleggen of door optimalisaties verrassend gedrag vertonen. Compilers gaan ervan uit dat ongedefinieerd gedrag (UB) niet voorkomt en optimaliseren op basis van die aanname, waardoor problemen soms alleen zichtbaar worden in release-builds of onder productieverkeer. Backend-incidenten kunnen voortkomen uit dangling pointers/referenties, use-after-free, schendingen van de levensduur van objecten, toegang buiten de grenzen (out-of-bounds), overloop van signed integers, dataraces, ongeldige casts, het lezen van ongeïnitialiseerde waarden, double frees of schendingen van strict-aliasing. Mitigatie omvat RAII (Resource Acquisition Is Initialization) en een helder ontwerp van eigenaarschap en levensduur, veiligere abstracties en grenscontroles, testen/fuzzing, code-reviews, statische analyse en sanitizers zoals ASan, UBSan en TSan.
12Leg virtuele functies, vtables, de kosten van dynamische dispatch, object slicing en de gevaren van virtuele destructors uit.
Een virtuele functie maakt runtimepolymorfisme mogelijk: wanneer deze wordt aangeroepen via een pointer of referentie van de basisklasse, wordt de implementatie voor het dynamische type van het object gekozen. De meeste implementaties slaan een verborgen `vptr` op in elk polymorf object dat wijst naar een vtable met adressen van virtuele functies voor dat dynamische type. De typische kosten bestaan uit een extra pointer in het object, een indirecte aanroep, mogelijke impact op cache en branch-prediction, en verminderde mogelijkheden voor inlining, hoewel compilers virtuele aanroepen soms kunnen devirtualiseren. Object slicing treedt op wanneer een afgeleid object als waarde wordt gekopieerd of opgeslagen als een basisobject, waardoor het afgeleide deel en het dynamische gedrag verloren gaan. Als het de bedoeling is dat een basisklasse wordt verwijderd via een pointer van die basisklasse, moet de destructor virtueel zijn; anders resulteert het verwijderen van een afgeleid object via die basispointer in ongedefinieerd gedrag.
13Beschrijf `std::chrono`-klokken, tijdstippen en tijdsduren voor servicetime-outs, metrieken en tijdstempels.
`std::chrono` modelleert tijd met klokken, `time_point`s en `duration`s. Een `duration` is een interval met een eenheid, zoals milliseconden of seconden. Een `time_point` is een punt op de tijdlijn van een specifieke klok. `steady_clock` is monotoon en moet worden gebruikt voor verstreken tijd, servicetime-outs, deadlines en latentiemetingen, omdat deze niet wordt beïnvloed door aanpassingen in de werkelijke kloktijd. `system_clock` vertegenwoordigt de burgerlijke of werkelijke kloktijd en is geschikt voor tijdstempels, logboekregistratie, datapersistentie en kalenderconversies, maar deze kan verspringen wanneer de systeemtijd wordt aangepast. Time-outs moeten doorgaans worden gebaseerd op `steady_clock::now() + duration`; machinetijdstempels moeten een duidelijk gedocumenteerd formaat voor de werkelijke tijd gebruiken, doorgaans UTC (Coordinated Universal Time) bij API- of opslaggrenzen.
14Beschrijf `std::format` en moderne opmaakvoorzieningen in vergelijking met iostreams en API's in `printf`-stijl.
`std::format` is de typeveilige opmaakvoorziening in C++20, geïnspireerd door fmtlib. Het gebruikt `{}`-vervangingsvelden en opmaakspecificaties om opgemaakte tekst te produceren zonder de toestandsafhankelijke invoegsyntaxis van iostreams of C-varargs in `printf`-stijl. Vergeleken met `printf` voorkomt het veel problemen met niet-overeenkomende opmaak of typen; vergeleken met iostreams is het vaak overzichtelijker en makkelijker samen te stellen. fmtlib is de veelgebruikte bibliotheek die voorafging aan `std::format` en deze heeft beïnvloed, en kan een bredere ondersteuning of nieuwere functies bieden. Door de gebruiker gedefinieerde typen kunnen worden opgemaakt via ondersteuning voor aangepaste formatters. Bij logging van grote volumes hangen de prestaties af van het vermijden van onnodige opmaak, conversies en allocaties, vooral voor uitgeschakelde logniveaus; API's die de opmaak uitstellen of eerst het logniveau controleren, hebben de voorkeur.
15Leg C++ waardecategorieën en perfecte forwarding uit, en waarom ze belangrijk zijn voor efficiënte generieke API's.
C++ waardecategorieën beschrijven expressies: lvalues hebben identiteit en er kan naar worden verwezen na de expressie; prvalues zijn pure rvalues zoals veel tijdelijke of berekende waarden; xvalues zijn objecten waarvan de levensduur afloopt en waarvan de resources mogelijk worden hergebruikt. Perfecte forwarding is een templatetechniek waarbij een forwarding reference wordt geaccepteerd, doorgaans `T&&` waarbij `T` wordt afgeleid, en wordt doorgestuurd met `std::forward<T>(arg)` zodat de waardecategorie van de aanroeper behouden blijft: lvalues blijven lvalues en rvalues blijven rvalues. Regels voor reference collapsing maken dit mogelijk. Dit is belangrijk voor generieke backend-API's, omdat wrappers, factories, dispatchers en emplace-achtige functies onnodige kopieën kunnen vermijden en de selectie van overloads en move-gedrag behouden.
16Beschrijf strategieën voor het voorkomen van deadlocks en het vergrendelen van meerdere mutexen met behulp van std::scoped_lock en std::lock.
Een deadlock wordt voorkomen door circulaire wachttijden te vermijden: verkrijg locks waar mogelijk in een consistente globale volgorde, of verkrijg meerdere mutexen met std::lock/std::scoped_lock, die een algoritme gebruiken om deadlocks te vermijden. std::scoped_lock lock(a, b, ...) is de eenvoudigste RAII (Resource Acquisition Is Initialization)-vorm voor het vergrendelen van meerdere mutexen en het automatisch ontgrendelen ervan bij het verlaten van het bereik. Bij gebruik van std::lock vergrendel je eerst de mutexen, waarna je RAII-wrappers toevoegt met std::adopt_lock, of je gebruikt std::unique_lock in combinatie met std::defer_lock. Houd kritieke secties kort en vermijd blokkerende operaties of onbekende callbackfuncties zolang je locks vasthoudt; het vermijden van deadlocks garandeert niet automatisch een eerlijke verdeling en voorkomt niet alle scenario's met livelocks of uithongering.
17Leg de gebruikspatronen van condition_variable uit voor het wachten op statuswijzigingen.
Gebruik `std::condition_variable` om te wachten tot een statuspredicaat verandert dat door een mutex wordt beschermd. De gedeelde status wordt gewijzigd terwijl dezelfde mutex wordt vastgehouden, waarna `notify_one` of `notify_all` de wachtende threads wekt. Wachtende threads moeten `cv.wait(lock, predicate)` of een vergelijkbare lus gebruiken, omdat valse wake-ups kunnen optreden en notificaties niet onafhankelijk van de predicaatstatus worden onthouden. `notify_one` wekt één wachtende thread; `notify_all` wekt alle wachtende threads en is geschikt voor statuswijzigingen die naar alle threads uitgezonden moeten worden, zoals bij het afsluiten van het systeem.
18Ontwerp een thread-veilige begrensde wachtrij met behulp van standaard C++-primitieven en definieer de API-garanties.
Een begrensde thread-veilige wachtrij kan worden gebouwd met een `std::mutex`, conditievariabelen, een container met vaste capaciteit en een shutdown/closed-vlag. `push` blokkeert, resulteert in een time-out of mislukt wanneer de wachtrij vol is, wat zorgt voor tegendruk; `pop` blokkeert wanneer de wachtrij leeg is. Beide bewerkingen moeten wachten op predicaten zoals `size < capacity || closed` en `!empty || closed`. Bij een shutdown of sluiting moeten geblokkeerde producenten en consumenten worden gewekt; weiger nieuwe `push`-bewerkingen en bepaal of consumenten de resterende items nog verwerken of onmiddellijk stoppen. De API moet het blokkeringsgedrag, de sluitingssemantiek, retourwaarden en garanties rondom gelijktijdigheid specificeren.
19Hoe voer je een schemamigratie uit voor een C++-service zonder downtime?
Gebruik een gefaseerde expand-contract-migratie. Maak eerst achterwaarts compatibele schema-uitbreidingen, zoals nullable kolommen of nieuwe tabellen, die de momenteel draaiende C++-service niet breken. Implementeer compatibele code die zowel oude als nieuwe representaties aankan, vul bestaande gegevens aan in kleine, gelimiteerde batches (backfilling), valideer de consistentie, schakel leesbewerkingen over naar het nieuwe schema, en verwijder oude kolommen of code pas later als er geen gedeployde versies meer afhankelijk van zijn. Vermijd bij grote tabellen langdurig blokkerende DDL (Data Definition Language), gebruik online/gelijktijdige bewerkingen waar ondersteund, bewaak vergrendelingen (locks), replicatie en systeembronnen, en zorg voor terugdraaiscenario's (rollback paths) die werken met deels geïmplementeerde code en deels gemigreerde gegevens.
20Hoe moet een C++-service lezen uit database-replica's en tegelijkertijd omgaan met verouderde gegevens en 'read-your-writes'-garanties?
Behandel het lezen uit replica's als een expliciete afweging in consistentie. Leesacties die verouderde gegevens (staleness) kunnen tolereren, mogen naar gezonde replica's gaan, maar 'read-your-writes', transactionele of versheid-kritieke leesacties moeten naar de primaire node gaan, tenzij bekend is dat de gekozen replica de relevante schrijfpositie heeft verwerkt, zoals een LSN (Log Sequence Number), tijdstempel of versie. Houd replica-achterstand (lag) en gezondheid bij, codeer de vereiste consistentie per eindpunt of request, en val terug op de primaire node, wacht op synchronisatie of laat de actie falen wanneer replica's de toegestane veroudering overschrijden. Documenteer het consistentiemodel zodat aanroepers weten welke leesacties verouderd kunnen zijn.