Preparazione C++ Middle

Domande di Colloquio per Backend C++ Middle

15 domande di colloquio sul C++ selezionate per sviluppatori backend Middle che devono discutere di prestazioni, gestione della proprietà e trade-off in produzione.

Inizia un Colloquio AI in C++ MiddleNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Gestione della Memoria

1Come funzionano le funzioni di rilascio personalizzate (custom deleter) negli smart pointer e quando sono utili per l'interoperabilità tra C e C++?

Una funzione di rilascio personalizzata è una logica di pulizia invocabile utilizzata da uno smart pointer al posto dell'operazione di `delete` predefinita quando il puntatore rilascia la risorsa di cui è proprietario. È utile per l'interoperabilità C/C++ quando una risorsa deve essere rilasciata con una funzione specifica come `fclose`, `close`, `curl_easy_cleanup`, `SSL_free`, `free` o una funzione di distruzione di una libreria. Per `unique_ptr`, il tipo del deleter fa parte del tipo di `unique_ptr` e può influire sulle sue dimensioni; i deleter senza stato possono essere omessi grazie alle ottimizzazioni, mentre i puntatori a funzione o i deleter con stato richiedono memoria aggiuntiva. Per `shared_ptr`, il deleter viene archiviato nel blocco di controllo (control block) ed eseguito quando l'ultimo proprietario forte (strong owner) rilascia l'oggetto. I custom deleter consentono alle risorse C di partecipare in modo sicuro all'idioma RAII (Resource Acquisition Is Initialization).

Prova a rispondere a questa domanda con un coach IA

2Spiega i meccanismi del blocco di controllo (control block) per weak_ptr e shared_ptr, includendo i cicli, enable_shared_from_this e i costi relativi al conteggio dei riferimenti (refcount).

Uno shared_ptr gestisce la proprietà condivisa tramite un blocco di controllo contenente i conteggi dei riferimenti forti e deboli, oltre a informazioni per la pulizia delle risorse come la funzione di rilascio personalizzata (deleter) e l'allocatore. La copia o la distruzione di oggetti shared_ptr incrementa o decrementa il conteggio dei riferimenti forti, tipicamente tramite operazioni atomiche, in modo che oggetti shared_ptr separati possano essere manipolati in sicurezza tra i thread, tuttavia ogni aggiornamento del refcount ha un costo. Quando il conteggio dei riferimenti forti raggiunge lo zero, l'oggetto gestito viene distrutto; il blocco di controllo rimane finché non vengono rimossi anche tutti i riferimenti deboli. Il weak_ptr punta allo stesso blocco di controllo senza estendere il ciclo di vita dell'oggetto; lock() restituisce uno shared_ptr se l'oggetto è ancora in vita, altrimenti restituisce uno shared_ptr vuoto. I cicli composti esclusivamente da shared_ptr causano leak di memoria perché i conteggi dei riferimenti forti non raggiungono mai lo zero, motivo per cui weak_ptr viene utilizzato per i puntatori all'indietro o i collegamenti observer. enable_shared_from_this consente a un oggetto già di proprietà di uno shared_ptr di creare un nuovo shared_ptr verso se stesso utilizzando il blocco di controllo esistente, evitando la creazione pericolosa di blocchi di controllo separati. I costi del refcount includono gli incrementi e i decrementi atomici, la contesa della cache, l'allocazione del blocco di controllo e l'overhead nelle parti di codice critiche per le prestazioni.

Prova a rispondere a questa domanda con un coach IA

Template

3Spiega le categorie di valore (value categories) e il perfect forwarding in C++, e perché sono importanti per API generiche ed efficienti.

Le categorie di valore in C++ descrivono le espressioni: gli lvalue hanno un'identità e possono essere riferiti dopo la valutazione dell'espressione; i prvalue sono rvalue puri come molti valori temporanei o calcolati; gli xvalue sono oggetti in scadenza le cui risorse possono essere riutilizzate. Il perfect forwarding è la tecnica dei template che consiste nell'accettare un riferimento di inoltro (forwarding reference), tipicamente `T&&` dove `T` viene dedotto, e nell'inoltrarlo con `std::forward<T>(arg)` affinché la categoria di valore del chiamante venga preservata: gli lvalue rimangono lvalue e gli rvalue rimangono rvalue. Le regole di collasso dei riferimenti (reference collapsing) rendono tutto ciò possibile. È importante per le API di backend generiche perché i wrapper, le factory, i dispatcher e le funzioni di tipo emplace possono evitare copie non necessarie e preservare la selezione degli overload e il comportamento di spostamento (move).

Prova a rispondere a questa domanda con un coach IA

Sistema dei Tipi

4Confronta `auto`, `decltype`, `decltype(auto)` e la deduzione degli argomenti dei template nel comune codice di backend.

`auto` utilizza una deduzione simile a quella dei template per le variabili: un semplice `auto` di solito rimuove le reference e il `const` di primo livello, a meno che la dichiarazione non li richieda esplicitamente, come in `auto&`, `const auto&` o `auto&&`. `decltype(expr)` ispeziona il tipo dichiarato o il tipo dell'espressione in modo più esatto: una id-expression senza parentesi restituisce il tipo dichiarato, mentre altre espressioni lvalue producono `T&`, le espressioni xvalue producono `T&&` e le espressioni prvalue producono `T`. `decltype(auto)` effettua la deduzione usando le regole di `decltype`, spesso per i tipi restituiti quando le reference devono essere preservate. La deduzione degli argomenti dei template è simile ad `auto` ma dipende dalla forma del parametro, come `T`, `T&`, `const T&` o `T&&`, e ha regole proprie. Gli inizializzatori tra parentesi graffe (braced initializers) rappresentano una differenza comune: `auto x = {1, 2}` deduce `std::initializer_list<int>`, mentre un semplice parametro di template generalmente non può dedurre `T` da un inizializzatore tra graffe semplice, a meno che il parametro non si aspetti un `initializer_list` o un altro tipo idoneo.

Prova a rispondere a questa domanda con un coach IA

5Confronta `std::variant` con il polimorfismo basato sull'ereditarietà per modellare messaggi o eventi eterogenei.

`std::variant` è un tipo valore che contiene esattamente un'alternativa da un insieme fisso e chiuso di tipi e viene comunemente gestito con `std::visit` o interrogazioni esplicite sul tipo. È utile per messaggi o eventi di protocollo quando l'insieme dei tipi di messaggio è noto e si desidera una gestione type-safe senza dispatch virtuale e, spesso, senza allocazione nell'heap per singolo oggetto. Il polimorfismo basato sull'ereditarietà utilizza una classe base e funzioni virtuali per il dispatch tramite un'interfaccia comune; è preferibile quando l'insieme dei tipi di messaggio derivati è aperto, estensibile in modo indipendente, simile a un plugin o nascosto dietro un'interfaccia stabile. `std::variant` favorisce tipi somma chiusi, località della memoria e controlli a tempo di compilazione; l'ereditarietà favorisce l'estensibilità, il polimorfismo a runtime e la progettazione basata su interfacce.

Prova a rispondere a questa domanda con un coach IA

Semantica del Linguaggio

6Cos'è il comportamento indefinito (undefined behavior) in C++, e come può manifestarsi negli incidenti di produzione nel backend?

Il comportamento indefinito è un comportamento per il quale lo standard C++ non impone alcun requisito a seguito del verificarsi di un'operazione non valida. Il programma può sembrare funzionare, andare in crash, corrompere i dati, esporre vulnerabilità di sicurezza o essere ottimizzato in comportamenti inaspettati. I compilatori presumono che il comportamento indefinito non si verifichi e ottimizzano il codice basandosi su questa assunzione, di conseguenza i problemi potrebbero manifestarsi solo nelle build di rilascio o sotto il traffico di produzione. Gli incidenti nel backend possono derivare da puntatori/riferimenti pendenti (dangling), use-after-free, violazioni del ciclo di vita degli oggetti, accessi fuori dai limiti (out-of-bounds), overflow di interi con segno, data race, cast non validi, letture non inizializzate, doppie deallocazioni (double free) o violazioni dello strict-aliasing. Le strategie di mitigazione includono l'idioma RAII (Resource Acquisition Is Initialization) e una progettazione chiara della proprietà e del ciclo di vita, astrazioni più sicure e controlli sui limiti, test/fuzzing, revisione del codice, analisi statica e l'utilizzo di sanitizer come ASan, UBSan e TSan.

Prova a rispondere a questa domanda con un coach IA

7Differenzia il comportamento indefinito, non specificato e definito dall'implementazione con esempi rilevanti per il backend.

Il comportamento indefinito (Undefined Behavior) significa che lo standard C++ non impone alcun requisito; i risultati possono includere crash, corruzione dei dati, bug di sicurezza o compilazione errata dipendente dall'ottimizzatore. Gli esempi includono l'accesso fuori dai limiti, l'uso di memoria liberata (use-after-free), l'overflow degli interi con segno e le data race. Il comportamento non specificato (Unspecified Behavior) significa che lo standard consente più di un risultato e l'implementazione non è tenuta a documentare quale venga scelto; un esempio comune è l'ordine di valutazione di molti argomenti di funzioni, per cui il codice non dovrebbe dipendere dal fatto che gli effetti collaterali vengano valutati in un ordine particolare. Il comportamento definito dall'implementazione (Implementation-Defined Behavior) significa che l'implementazione deve scegliere e documentare il comportamento; gli esempi includono il segno del normale `char`, le dimensioni e gli intervalli di alcuni tipi fondamentali entro i limiti standard e alcuni comportamenti dello scorrimento a destra con segno (signed right shift). Nel codice di backend, l'UB (Undefined Behavior) è un rischio per la correttezza e la sicurezza, mentre i comportamenti non specificati e definiti dall'implementazione sono rischi per la portabilità che dovrebbero essere evitati o isolati nella logica del protocollo, di archiviazione e multipiattaforma.

Prova a rispondere a questa domanda con un coach IA

Gestione degli Errori

8Spiega i livelli di sicurezza rispetto alle eccezioni e come `noexcept` influisce sulle operazioni di spostamento, sui contenitori e sulla generazione del codice.

I livelli di sicurezza rispetto alle eccezioni descrivono cosa rimane valido se un'operazione lancia un'eccezione. La garanzia di base (basic guarantee) significa che gli invarianti vengono preservati e le risorse non vengono perse, sebbene lo stato possa essere cambiato. La garanzia forte (strong guarantee) implica una semantica di commit-or-rollback: in caso di fallimento, lo stato osservabile rimane invariato. La garanzia di assenza di eccezioni (nothrow guarantee) significa che l'operazione non lancia eccezioni. Le tecniche comuni includono RAII (Resource Acquisition Is Initialization), l'esecuzione di operazioni su oggetti temporanei, il copy-and-swap e l'ordinamento delle mutazioni in modo che il commit avvenga solo dopo il successo delle operazioni che possono lanciare eccezioni. `noexcept` è un contratto: se una funzione `noexcept` lancia un'eccezione, viene chiamato `std::terminate`. Influisce anche sul codice generico e sui contenitori: ad esempio, durante la riallocazione `std::vector` può spostare gli elementi quando il costruttore di spostamento è `noexcept`; altrimenti potrebbe copiarli, spesso tramite `std::move_if_noexcept`, per preservare le garanzie sulle eccezioni. `noexcept` può anche aiutare la generazione o l'ottimizzazione del codice riducendo i percorsi di propagazione delle eccezioni necessari, ma solo quando la promessa è corretta.

Prova a rispondere a questa domanda con un coach IA

9Spiega `std::expected` o tipi di risultato (outcome) equivalenti come alternative alle eccezioni per le operazioni che possono fallire.

`std::expected<T, E>` rappresenta un valore di successo `T` o un errore esplicito `E`, rendendo il fallimento parte del tipo di ritorno della funzione invece di affidarsi alla propagazione delle eccezioni. È utile per operazioni soggette a errori in cui questi sono previsti e i chiamanti dovrebbero gestirli localmente, come il parsing, la validazione, le chiamate di rete, le ricerche nello storage e le operazioni di backend ripetibili. Il tipo di errore dovrebbe contenere informazioni strutturate come un codice di errore, una categoria, la possibilità di ritentare l'operazione, un messaggio o una mappatura HTTP/RPC (Hypertext Transfer Protocol / Remote Procedure Call). I tipi expected o outcome si compongono controllando e propagando gli errori, e nelle API in stile C++23 possono utilizzare operazioni monadiche come `and_then`, `transform` e `or_else` per concatenare i passaggi senza condizionali profondamente annidati. Rispetto alle eccezioni, rendono espliciti il flusso di controllo e i contratti delle API, e funzionano bene in contesti privi di eccezioni o attraverso i confini dell'ABI (Application Binary Interface); le eccezioni potrebbero rimanere appropriate per fallimenti rari, non locali o veramente eccezionali a seconda delle policy del progetto.

Prova a rispondere a questa domanda con un coach IA

Ciclo di Vita degli Oggetti

10Descrivi le regole sul ciclo di vita degli oggetti per gli oggetti automatici, dinamici, temporanei e referenziati in modo asincrono.

Gli oggetti automatici vivono dalla costruzione fino alla fine del loro ambito (scope); i puntatori o i riferimenti a essi diventano penzolanti (dangling) dopo l'uscita da tale ambito. Gli oggetti dinamici vivono dall'allocazione/costruzione fino a quando non vengono distrutti esplicitamente o un oggetto proprietario li distrugge; i puntatori raw e i riferimenti non ne estendono il ciclo di vita. Gli oggetti temporanei di solito vivono fino alla fine dell'intera espressione, con regole specifiche di estensione del ciclo di vita quando associati a dei riferimenti, ma non in ogni caso d'uso. Il lavoro asincrono come callback, thread, timer o coroutine potrebbe essere eseguito dopo che l'oggetto referenziato è stato distrutto, quindi le catture e i riferimenti memorizzati richiedono una gestione esplicita del ciclo di vita per evitare riferimenti dangling e bug di tipo use-after-free.

Prova a rispondere a questa domanda con un coach IA

11Descrivi i problemi legati all'ordine di inizializzazione statica e in che modo constinit, le "magic statics" e la dependency injection li mitigano.

I problemi relativi all'ordine di inizializzazione statica si verificano perché gli oggetti statici o con ambito di namespace inizializzati dinamicamente in diverse unità di traduzione hanno un ordine di inizializzazione relativo non specificato. Una variabile globale potrebbe usarne un'altra prima che quest'ultima sia stata costruita; anche l'ordine di distruzione può creare problemi simili durante la terminazione del programma. `constinit` impone che una variabile statica o thread-local abbia un'inizializzazione statica o costante (altrimenti il programma è malformato), evitando la dipendenza dall'ordine di inizializzazione dinamica per quella variabile. Le "magic statics", o variabili statiche locali alle funzioni, vengono inizializzate al primo utilizzo e sono thread-safe a partire dal C++11. La dependency injection (iniezione delle dipendenze) evita dipendenze globali nascoste costruendo gli oggetti in un ordine controllato e passando le dipendenze in modo esplicito.

Prova a rispondere a questa domanda con un coach IA

12Cosa sono l'elisione della copia (copy elision), la NRVO (Named Return Value Optimization) e la RVO (Return Value Optimization), e quando è possibile farvi affidamento?

L'elisione della copia consiste nella costruzione di un oggetto direttamente nella sua destinazione finale, invece di creare un oggetto temporaneo separato per poi copiarlo o spostarlo. La RVO si riferisce solitamente alla restituzione di un oggetto temporaneo senza nome o di un prvalue, come return T{}; in C++17 e versioni successive, molti di questi casi sono obbligatori perché il prvalue inizializza direttamente l'oggetto risultato. La NRVO consiste nella restituzione di un oggetto locale con nome, come return x;; al compilatore è consentito costruire tale oggetto locale direttamente nello slot di ritorno del chiamante, ma questo non è garantito. È possibile fare affidamento sull'elisione obbligatoria dei prvalue di C++17 nei casi specificati, ma non si può avere la certezza che la NRVO avvenga sempre; evita di usare std::move sul valore di ritorno di un oggetto locale con nome, poiché può impedire la NRVO.

Prova a rispondere a questa domanda con un coach IA

Libreria Standard

13In che modo i requisiti del comparatore, dell'uguaglianza e della funzione di hash influiscono sulla correttezza dei contenitori associativi?

I contenitori associativi si affidano al confronto, all'uguaglianza e all'hashing per definire l'identità delle chiavi e mantenere i propri invarianti interni. I contenitori ordinati come `std::map` richiedono che il comparatore imponga un ordinamento debole rigoroso; le chiavi sono considerate equivalenti quando nessuna delle due risulta minore dell'altra, non necessariamente tramite `operator==`. I contenitori non ordinati richiedono che l'uguaglianza sia una relazione di equivalenza e che qualsiasi coppia di chiavi considerata uguale produca lo stesso valore di hash. Le chiavi non devono essere modificate mentre sono memorizzate in un modo che alteri il loro ordinamento, la loro uguaglianza o il loro hash, poiché ciò può rendere scorretto il comportamento delle operazioni di ricerca, eliminazione e verifica dell'unicità.

Prova a rispondere a questa domanda con un coach IA

14Spiega la ricerca eterogenea nei contenitori associativi ordinati e non ordinati.

La ricerca eterogenea permette di cercare all'interno di un contenitore associativo utilizzando un tipo diverso da quello della sua chiave, evitando la costruzione di chiavi temporanee. Nei contenitori ordinati, questo richiede un comparatore trasparente, per esempio std::less<> o un comparatore personalizzato con un marcatore is_transparent. Nei contenitori non ordinati, richiede funtori trasparenti sia per l'hash che per l'uguaglianza, in grado di gestire in modo coerente il tipo della chiave e il tipo utilizzato per la ricerca. Un tipico esempio in ambito backend è un contenitore indicizzato tramite std::string che può essere ricercato utilizzando std::string_view o const char* senza dover allocare una std::string temporanea.

Prova a rispondere a questa domanda con un coach IA

Concorrenza

15Spiega il modello di memoria in C++: data race, happens-before e garanzie di sincronizzazione.

Il modello di memoria in C++ definisce quando le operazioni in thread diversi sono ordinate e quando le scritture diventano visibili. Una data race si verifica quando due thread accedono simultaneamente alla stessa locazione di memoria, almeno uno degli accessi è una scrittura, e gli accessi non sono ordinati da una relazione happens-before o altrimenti resi sicuri tramite operazioni atomiche; una data race causa un comportamento indefinito. La relazione happens-before è la relazione di ordinamento che rende visibili gli effetti collaterali precedenti alle operazioni successive. Le operazioni di sincronizzazione creano questo ordinamento: per esempio, sbloccare un mutex si sincronizza con (synchronizes-with) un successivo blocco andato a buon fine dello stesso mutex, e opportune operazioni atomiche di release/acquire possono sincronizzare tra thread. I programmi corretti utilizzano mutex, atomici o altri meccanismi di sincronizzazione per stabilire una relazione happens-before per lo stato condiviso.

Prova a rispondere a questa domanda con un coach IA