1Hoe 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).
2Leg 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.
3Leg 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.
4Vergelijk `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.
5Vergelijk std::variant met op overerving gebaseerd polymorfisme voor het modelleren van heterogene berichten of gebeurtenissen.
std::variant is een waardetype dat exact één alternatief uit een vaste, gesloten verzameling typen bevat en meestal wordt afgehandeld met std::visit of expliciete typecontroles. Het is nuttig voor protocolberichten of gebeurtenissen wanneer de reeks van berichttypen bekend is en je een typeveilige afhandeling wilt zonder virtuele dispatch, en vaak zonder heap-allocatie per object. Op overerving gebaseerd polymorfisme gebruikt een basisklasse en virtuele functies om via een gemeenschappelijke interface aan te roepen; dit is beter wanneer de verzameling van afgeleide berichttypen open is, onafhankelijk uitbreidbaar is, plug-in-achtig is of verborgen is achter een stabiele interface. Variant is optimaal voor gesloten somtypen, datalokaliteit en controles tijdens het compileren; overerving is beter voor uitbreidbaarheid, polymorfisme tijdens de uitvoering en een op interfaces gebaseerd ontwerp.
6Wat 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.
7Wat is het verschil tussen ongedefinieerd, ongespecificeerd en implementatie-afhankelijk gedrag? Geef voorbeelden die relevant zijn voor de backend.
Ongedefinieerd gedrag (undefined behavior, of UB) betekent dat de C++-standaard geen vereisten oplegt; mogelijke resultaten zijn onder meer crashes, datacorruptie, beveiligingslekken of onjuiste compilaties die afhankelijk zijn van de *optimizer*. Voorbeelden hiervan zijn toegang buiten de geheugengrenzen, het gebruik van vrijgegeven geheugen (use-after-free), *overflow* van een signed integer, en data races. Ongespecificeerd gedrag (unspecified behavior) betekent dat de standaard meerdere uitkomsten toestaat en de implementatie niet hoeft vast te leggen welke wordt gekozen; een veelvoorkomend voorbeeld is de evaluatievolgorde van functieargumenten. Code mag er dus niet van uitgaan dat neveneffecten in een specifieke volgorde worden geëvalueerd. Implementatie-afhankelijk gedrag (implementation-defined behavior) houdt in dat de implementatie het gedrag moet kiezen en documenteren; voorbeelden zijn het al dan niet signed zijn van een standaard `char`, de groottes of bereiken van sommige basistypen binnen de limieten van de standaard, en de precieze werking van een signed *right shift*. In backendcode vormt UB een risico voor correctheid en veiligheid, terwijl ongespecificeerd en implementatie-afhankelijk gedrag risico's inhouden voor overdraagbaarheid. Deze moeten worden vermeden of geïsoleerd in protocol-, opslag- en platformonafhankelijke logica.
8Leg uit wat de niveaus van veiligheid bij uitzonderingen zijn en hoe `noexcept` van invloed is op verplaatsingsoperaties, containers en codegeneratie.
Niveaus van veiligheid bij uitzonderingen beschrijven wat geldig blijft wanneer een bewerking een uitzondering opgooit. De basisgarantie houdt in dat invarianten behouden blijven en er geen bronnen lekken, hoewel de status veranderd kan zijn. De sterke garantie betekent commit-of-rollback-semantiek: bij falen blijft de waarneembare status ongewijzigd. De nothrow-garantie betekent dat de bewerking geen uitzonderingen opgooit. Veelgebruikte technieken zijn onder meer RAII (Resource Acquisition Is Initialization), werk uitvoeren op tijdelijke objecten, copy-and-swap, en het ordenen van mutaties zodat de definitieve wijziging pas plaatsvindt nadat code die uitzonderingen kan opgooien succesvol is afgerond. `noexcept` is een contract: als een `noexcept`-functie toch een uitzondering opgooit, wordt `std::terminate` aangeroepen. Het heeft ook invloed op generieke code en containers: tijdens herallocatie kan `std::vector` bijvoorbeeld elementen verplaatsen als de verplaatsingsconstructor `noexcept` is; anders kan deze kopiëren, vaak via `std::move_if_noexcept`, om de garanties rond uitzonderingen te behouden. `noexcept` kan ook helpen bij codegeneratie of optimalisatie door het aantal benodigde paden voor de propagatie van uitzonderingen te verminderen, maar alleen wanneer deze belofte klopt.
9Leg `std::expected` of vergelijkbare 'outcome'-typen uit als alternatieven voor uitzonderingen bij bewerkingen die kunnen falen.
`std::expected<T, E>` vertegenwoordigt ofwel een succesvolle waarde `T` of een expliciete fout `E`, waardoor het falen deel uitmaakt van het retourtype van de functie in plaats van te vertrouwen op de propagatie van uitzonderingen. Het is nuttig voor bewerkingen die kunnen falen waarbij fouten worden verwacht en aanroepers deze lokaal moeten afhandelen, zoals parsing, validatie, netwerkaanroepen, opslagzoekopdrachten en herhaalbare backendbewerkingen. Het fouttype moet gestructureerde informatie bevatten, zoals een foutcode, categorie, herhaalbaarheid, foutmelding of HTTP/RPC-toewijzing (RPC = Remote Procedure Call). `expected`- of `outcome`-typen kunnen worden samengesteld door fouten te controleren en door te geven, en in C++23-stijl API's kunnen ze monadische bewerkingen gebruiken zoals `and_then`, `transform` en `or_else` om stappen aaneen te schakelen zonder diep geneste conditionele logica. Vergeleken met uitzonderingen maken ze de controleflow en API-contracten expliciet, en werken ze goed over grenzen heen waar geen uitzonderingen zijn toegestaan of ABI-grenzen (Application Binary Interface). Afhankelijk van het projectbeleid kunnen uitzonderingen nog steeds geschikt zijn voor zeldzame, niet-lokale of werkelijk uitzonderlijke storingen.
10Beschrijf de regels rondom de levensduur van objecten voor automatische, dynamische, tijdelijke en asynchroon gerefereerde objecten.
Automatische objecten leven vanaf hun constructie tot het einde van hun bereik; pointers of referenties hiernaar worden na het verlaten van dat bereik ongeldig (dangling). Dynamische objecten leven vanaf geheugentoewijzing of constructie totdat ze expliciet worden vernietigd of totdat een object dat eigenaar is ze vernietigt; raw pointers en referenties verlengen de levensduur niet. Tijdelijke objecten leven doorgaans tot het einde van de volledige expressie, met specifieke regels voor levensduurverlenging wanneer ze aan referenties worden gebonden, maar dit geldt niet in elk scenario. Asynchrone taken zoals callbacks, threads, timers of coroutines kunnen mogelijk nog worden uitgevoerd nadat het gerefereerde object is vernietigd. Daarom vereisen captures en opgeslagen referenties expliciet levensduurbeheer om dangling referenties en use-after-free fouten te voorkomen.
11Beschrijf problemen met de volgorde van statische initialisatie en hoe `constinit`, magic statics en dependency injection deze verminderen.
Problemen met de volgorde van statische initialisatie ontstaan doordat dynamisch geïnitialiseerde objecten met een namespace-bereik of statische objecten in verschillende vertaaleenheden een ongespecificeerde relatieve initialisatievolgorde hebben. De ene globale variabele kan een andere gebruiken voordat deze is geconstrueerd; de volgorde van destructie kan bij het afsluiten vergelijkbare problemen veroorzaken. `constinit` dwingt af dat een statische of thread-lokale variabele statische/constante initialisatie heeft, anders is het programma ongeldig, wat de afhankelijkheid van de dynamische initialisatievolgorde voor die variabele voorkomt. Magic statics, of functie-lokale statics, worden bij het eerste gebruik geïnitialiseerd en zijn veilig voor threads sinds C++11. Dependency injection voorkomt verborgen globale afhankelijkheden door objecten in een gecontroleerde volgorde te construeren en afhankelijkheden expliciet door te geven.
12Wat zijn copy elision, NRVO (Named Return Value Optimization) en RVO (Return Value Optimization), en wanneer kun je erop vertrouwen?
Copy elision betekent dat een object direct op zijn uiteindelijke bestemming wordt geconstrueerd, in plaats van een apart tijdelijk object te creëren en dit te kopiëren of te verplaatsen. RVO verwijst meestal naar het retourneren van een naamloos tijdelijk object of prvalue, zoals `return T{};`; in C++17 en later zijn veel van deze gevallen verplicht omdat de prvalue het resultaatobject direct initialiseert. NRVO is het retourneren van een benoemd lokaal object, zoals `return x;`; het is de compiler toegestaan om dat lokale object direct in de retourpositie van de aanroeper te construeren, maar dit is niet gegarandeerd. Je kunt vertrouwen op verplichte prvalue-elision in C++17 in de gespecificeerde gevallen, maar niet op de garantie dat NRVO altijd plaatsvindt; vermijd `std::move` op een benoemde lokale retourwaarde omdat dit NRVO kan voorkomen.
13Hoe beïnvloeden de vereisten voor comparators, gelijkheid en hashers de correctheid van associatieve containers?
Associatieve containers vertrouwen op vergelijking, gelijkheid en hashing om de identiteit van een sleutel te definiëren en hun interne invarianten te behouden. Geordende containers zoals `std::map` vereisen dat de comparator een strikte zwakke ordening (strict weak ordering) oplegt; sleutels worden als equivalent beschouwd wanneer geen van beide kleiner is dan de ander, niet noodzakelijkerwijs op basis van `operator==`. Ongeordende containers vereisen dat gelijkheid een equivalentierelatie is, en elke twee sleutels die als gelijk worden beschouwd, moeten dezelfde hashwaarde produceren. Sleutels mogen niet worden gemuteerd zolang ze opgeslagen zijn op een manier die hun ordening, gelijkheid of hashwaarde verandert, omdat dit het gedrag voor opzoeken, verwijderen en uniciteit incorrect kan maken.
14Leg 'heterogeneous lookup' uit in geordende en ongeordende associatieve containers.
Met heterogeen zoeken (heterogeneous lookup) kan in een associatieve container worden gezocht met een ander type dan het sleuteltype, waardoor het tijdelijk aanmaken van een sleutel wordt voorkomen. In geordende containers vereist dit een transparante vergelijker (comparator), bijvoorbeeld `std::less<>` of een aangepaste vergelijker met een `is_transparent`-markering. In ongeordende containers vereist het zowel transparante hash- als transparante gelijkheidsfunctoren die consistent met het sleuteltype en het zoektype kunnen omgaan. Een veelvoorkomend voorbeeld in de backend is een container met `std::string` als sleutel, waarin kan worden gezocht met `std::string_view` of `const char*` zonder een tijdelijke `std::string` te alloceren.
15Leg het C++ geheugenmodel uit: data races, happens-before en synchronisatiegaranties.
Het C++ geheugenmodel definieert wanneer bewerkingen in verschillende threads worden geordend en wanneer schrijfacties zichtbaar worden. Een data race ontstaat wanneer twee threads gelijktijdig toegang hebben tot dezelfde geheugenlocatie, ten minste één daarvan een schrijfactie is, en de bewerkingen niet geordend zijn door een 'happens-before'-relatie of niet veilig zijn gemaakt door atomaire bewerkingen; een data race leidt tot ongedefinieerd gedrag. Happens-before is de ordeningsrelatie die eerdere neveneffecten zichtbaar maakt voor latere bewerkingen. Synchronisatiebewerkingen creëren deze ordening: het ontgrendelen van een mutex resulteert bijvoorbeeld in een 'synchronizes-with'-relatie bij een latere, succesvolle vergrendeling van dezelfde mutex, en geschikte atomaire release/acquire-bewerkingen kunnen synchroniseren tussen threads. Correcte programma's gebruiken mutexes, atomaire variabelen of andere vormen van synchronisatie om happens-before tot stand te brengen voor gedeelde status.