Preparazione ai colloqui C++

Domande di Colloquio per Sviluppatori Backend C++

Una selezione di domande di colloquio sul C++ per sviluppatori backend, raggruppate per argomento ed estratte dallo stesso catalogo di domande che alimenta le simulazioni su EngineerSpeak.

Inizia un Colloquio AI per Backend C++Nessuna 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.

Resource Management

1Spiega RAII (Resource Acquisition Is Initialization) e come modella la gestione delle risorse sicura rispetto alle eccezioni nei servizi di backend in C++.

RAII (Resource Acquisition Is Initialization) significa che un oggetto C++ possiede una risorsa e la rilascia tramite il proprio distruttore. Poiché gli oggetti locali vengono distrutti automaticamente quando termina il loro ciclo di vita o ambito di visibilità, incluso durante lo srotolamento dello stack (stack unwinding) in caso di eccezioni, RAII garantisce una pulizia deterministica e rende i percorsi di errore sicuri rispetto alle eccezioni. Nei servizi di backend questo si applica non solo alla memoria, ma anche a descrittori di file, socket, lock di mutex, handle di database, transazioni e altre risorse del sistema operativo o dell'applicazione.

Prova a rispondere a questa domanda con un coach IA

Gestione della Memoria

2Confronta `std::unique_ptr` e `std::shared_ptr` e descrivi quando ciascuno è appropriato nelle API di backend.

`std::unique_ptr` rappresenta la proprietà esclusiva: è leggero, supporta lo spostamento ma non la copia, ed è appropriato per un singolo proprietario o per le API che trasferiscono la proprietà. `std::shared_ptr` rappresenta la proprietà condivisa: è copiabile e mantiene in vita un oggetto tramite il conteggio dei riferimenti finché l'ultimo proprietario forte non lo rilascia. Nelle API di backend, usa `unique_ptr` quando la proprietà viene trasferita, `shared_ptr` solo quando più proprietari indipendenti devono estenderne il ciclo di vita, e i riferimenti o i puntatori grezzi per l'accesso senza acquisirne la proprietà. L'uso di `std::make_shared` è generalmente preferito quando si creano oggetti `shared_ptr` perché è efficiente e sicuro rispetto alle eccezioni.

Prova a rispondere a questa domanda con un coach IA

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

4Spiega 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

Sistema dei Tipi

5Che cos'è la semantica di spostamento (move semantics) e come si implementano correttamente un costruttore di spostamento e un operatore di assegnazione per spostamento?

La semantica di spostamento permette a C++ di trasferire risorse da oggetti temporanei o altrimenti sacrificabili invece di copiarle. Utilizza i riferimenti rvalue come `T&&` e `std::move`, che è un cast che permette di selezionare gli overload di spostamento; `std::move` di per sé non sposta nulla. Un costruttore di spostamento (move constructor) corretto inizializza un nuovo oggetto prendendo le risorse dell'oggetto sorgente e lasciando quest'ultimo in uno stato valido, distruttibile e assegnabile. Un operatore di assegnazione per spostamento (move assignment operator) corretto trasferisce le risorse in un oggetto esistente, gestisce o tollera l'auto-assegnazione, rilascia o riutilizza la risorsa corrente della destinazione, acquisisce la risorsa sorgente e lascia il sorgente in uno stato sicuro. Le operazioni di spostamento dovrebbero spesso essere dichiarate `noexcept` affinché i container della libreria standard possano utilizzarle durante la riallocazione, preservando le garanzie di sicurezza rispetto alle eccezioni.

Prova a rispondere a questa domanda con un coach IA

6Descrivi la const-correctness nelle API C++ e come progettare in modo efficace le funzioni membro const.

La const-correctness significa esprimere attraverso il sistema dei tipi quali operazioni non modificano lo stato osservabile o logico di un oggetto. Una funzione membro const ha l'oggetto this qualificato come const, pertanto non può modificare i membri dati non mutabili o chiamare funzioni membro non const sullo stesso oggetto. Una buona progettazione delle API marca come const le interrogazioni di sola lettura, restituisce valori o riferimenti/puntatori const quando appropriato, ed evita di esporre lo stato interno mutabile dalle funzioni const. La parola chiave mutable dovrebbe essere riservata ai dettagli implementativi che non alterano lo stato logico, come cache, valori calcolati in modalità lazy, metriche o mutex. const rappresenta un contratto dell'API sulla mutazione, non una garanzia automatica di sicurezza nei thread (thread-safety); le garanzie di concorrenza richiedono un'implementazione e una documentazione separate.

Prova a rispondere a questa domanda con un coach IA

7Cosa sono i tipi di sola movimentazione (move-only) e in che modo influenzano i confini delle API per socket, handle di file e lock?

I tipi move-only sono tipi che non possono essere copiati ma possono essere spostati. Sono comuni per il possesso esclusivo (unique ownership) di risorse come socket, descrittori di file, handle di file, lock e `std::unique_ptr`. Le operazioni di copia sono eliminate (deleted) per evitare il possesso duplicato e il doppio rilascio (double release); le operazioni di spostamento (move) trasferiscono il possesso e lasciano la sorgente in uno stato valido ma solitamente vuoto/senza possesso. Le API dovrebbero rendere espliciti i confini del possesso: le factory possono restituire oggetti move-only per valore, le funzioni che acquisiscono il possesso possono accettarli per valore o per rvalue reference, e le funzioni che si limitano a ispezionarli dovrebbero accettare riferimenti, puntatori o altri handle di prestito. I container possono memorizzare valori move-only quando gli elementi vengono inseriti o riallocati tramite spostamento.

Prova a rispondere a questa domanda con un coach IA

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

Ciclo di Vita degli Oggetti

9Descrivi la Regola dello Zero, la Regola del Tre e la Regola del Cinque e quando si applica ciascuna di esse.

Regola dello Zero (Rule of Zero): preferire classi che non dichiarano operazioni personalizzate di distruzione, copia o spostamento; lasciare che i membri basati su RAII (Resource Acquisition Is Initialization), come `std::string`, `std::vector`, `std::unique_ptr`, e i wrapper di file/socket, gestiscano le risorse. Regola del Tre (Rule of Three): se una classe gestisce manualmente una risorsa e richiede un distruttore, un costruttore di copia o un operatore di assegnazione per copia personalizzati, di solito ha bisogno di tutti e tre per definire un comportamento corretto di copia e di proprietà (ownership). Regola del Cinque (Rule of Five): in C++11 e successivi, tali tipi dovrebbero considerare anche il costruttore di spostamento e l'operatore di assegnazione per spostamento. Utilizza la Regola dello Zero per la maggior parte dei tipi dell'applicazione; utilizza la Regola del Tre/Cinque quando il tipo possiede direttamente una risorsa o ha una semantica di proprietà e ciclo di vita non banale.

Prova a rispondere a questa domanda con un coach IA

Semantica del Linguaggio

10Descrivi le forme di inizializzazione in C++ e le insidie comuni, inclusi `initializer_list` e gli aggregati.

Il C++ presenta diverse forme di inizializzazione. L'inizializzazione predefinita (default initialization), come `T x;`, chiama un costruttore predefinito per i tipi di classe ma lascia le variabili fondamentali automatiche non inizializzate. L'inizializzazione per valore (value initialization), come `T x{};` o `T()`, inizializza a zero dove applicabile prima dell'inizializzazione del costruttore o dei membri. L'inizializzazione tramite lista (list initialization) utilizza le parentesi graffe, rifiuta le conversioni con perdita di precisione (narrowing conversions) e ha regole speciali per la risoluzione dell'overload, inclusa una forte preferenza per i costruttori `std::initializer_list` validi. L'inizializzazione degli aggregati inizializza i membri dell'aggregato direttamente tramite parentesi graffe; il C++20 supporta anche gli inizializzatori designati (designated initializers) per gli aggregati, in ordine di dichiarazione. Le insidie comuni includono scalari locali non inizializzati, scelte inaspettate nell'overload di `initializer_list`, errori di narrowing con le parentesi graffe, il problema del 'most-vexing parse' con le parentesi tonde e cambiamenti di comportamento quando un tipo smette di essere un aggregato.

Prova a rispondere a questa domanda con un coach IA

11Cos'è 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

Object Model

12Spiega le funzioni virtuali, le vtable, il costo del dynamic dispatch, l'object slicing e i pericoli dei distruttori virtuali.

Una funzione virtuale abilita il polimorfismo a tempo di esecuzione: quando viene chiamata tramite un puntatore o un riferimento alla classe base, viene selezionata l'implementazione per il tipo dinamico dell'oggetto. La maggior parte delle implementazioni memorizza un vptr (puntatore virtuale) nascosto in ogni oggetto polimorfico che punta a una vtable (tabella virtuale) contenente gli indirizzi delle funzioni virtuali per quel tipo dinamico. Il costo tipico consiste in un puntatore aggiuntivo nell'oggetto, una chiamata indiretta, un possibile impatto sulla cache e sulla predizione delle diramazioni (branch prediction), oltre a ridotte opportunità di inlining, sebbene i compilatori possano a volte devirtualizzare. L'object slicing si verifica quando un oggetto derivato viene copiato o memorizzato per valore come oggetto base, perdendo la parte derivata e il comportamento dinamico. Se una classe base è destinata a essere eliminata tramite un puntatore alla classe base, il suo distruttore deve essere virtuale; altrimenti, l'eliminazione di un oggetto derivato tramite quel puntatore alla base porta a un comportamento indefinito.

Prova a rispondere a questa domanda con un coach IA

Libreria Standard

13Descrivi i clock, i `time_point` e le `duration` di `std::chrono` per i timeout dei servizi, le metriche e i timestamp.

`std::chrono` modella il tempo attraverso clock, `time_point` e `duration`. Una `duration` è un intervallo associato a un'unità di misura, come millisecondi o secondi. Un `time_point` è un punto sulla linea temporale di uno specifico clock. `steady_clock` è monotonico e dovrebbe essere utilizzato per il tempo trascorso, i timeout dei servizi, le scadenze (deadline) e le misurazioni della latenza, poiché non è influenzato dalle modifiche all'orologio di sistema (wall-clock). `system_clock` rappresenta il tempo civile o di sistema ed è appropriato per timestamp, logging, persistenza e conversioni di calendario, ma può subire dei salti quando l'ora del sistema viene regolata. I timeout dovrebbero generalmente basarsi su `steady_clock::now() + duration`; i timestamp di sistema dovrebbero utilizzare un formato di tempo civile chiaramente documentato, tipicamente UTC ai confini delle API o dello storage.

Prova a rispondere a questa domanda con un coach IA

14Descrivi `std::format` e le moderne funzionalità di formattazione rispetto agli iostream e alle API in stile `printf`.

`std::format` è la funzionalità di formattazione sicura rispetto ai tipi (type-safe) introdotta in C++20, ispirata a fmtlib. Utilizza campi di sostituzione `{}` e specifiche di formattazione per produrre testo formattato senza la sintassi di inserimento con stato (stateful) degli iostream o gli argomenti variabili (varargs) C in stile `printf`. Rispetto a `printf`, evita molti problemi di mancata corrispondenza tra formato e tipo; rispetto agli iostream, è spesso più chiaro e facile da comporre. fmtlib è la libreria ampiamente utilizzata che ha preceduto e influenzato `std::format` e può fornire un supporto più ampio o funzionalità più recenti. I tipi definiti dall'utente possono essere formattati definendo un formattatore personalizzato. Nei log ad alto volume, le prestazioni dipendono dall'evitare formattazioni, conversioni e allocazioni non necessarie, specialmente per i livelli di log disabilitati; sono preferibili API che differiscono la formattazione o controllano prima il livello di log.

Prova a rispondere a questa domanda con un coach IA

Template

15Spiega 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

Concorrenza

16Descrivi le strategie per la prevenzione dei deadlock e il blocco su mutex multipli utilizzando `std::scoped_lock` e `std::lock`.

Il deadlock viene prevenuto evitando le attese circolari: quando possibile, acquisisci i lock in un ordine globale coerente, oppure acquisisci più mutex con `std::lock`/`std::scoped_lock`, che utilizzano un algoritmo per evitare i deadlock. `std::scoped_lock lock(a, b, ...)` è la forma RAII (Resource Acquisition Is Initialization) più semplice per acquisire il lock su diversi mutex e sbloccarli automaticamente all'uscita dall'ambito (scope). Con `std::lock`, per prima cosa blocchi i mutex, poi associ dei wrapper RAII usando `std::adopt_lock`, oppure utilizzi `std::unique_lock` con `std::defer_lock`. Mantieni brevi le sezioni critiche ed evita operazioni bloccanti o funzioni di callback sconosciute mentre mantieni i lock; l'evitamento del deadlock non garantisce automaticamente l'equità (fairness) né previene tutti gli scenari di livelock o inedia (starvation).

Prova a rispondere a questa domanda con un coach IA

17Spiega i pattern di utilizzo di condition_variable per l'attesa dei cambiamenti di stato.

Usa `std::condition_variable` per attendere che un predicato di stato protetto da mutex cambi. Lo stato condiviso viene modificato mantenendo bloccato lo stesso mutex, poi `notify_one` o `notify_all` risveglia i thread in attesa. I thread in attesa dovrebbero usare `cv.wait(lock, predicate)` o un ciclo equivalente perché i risvegli possono essere spuri e le notifiche non vengono ricordate in modo indipendente dallo stato del predicato. `notify_one` risveglia un solo thread in attesa; `notify_all` risveglia tutti i thread in attesa ed è appropriato per cambiamenti di stato in broadcast, come l'arresto del sistema.

Prova a rispondere a questa domanda con un coach IA

18Progetta una coda limitata thread-safe utilizzando le primitive standard del C++ e definisci le garanzie della sua API.

Una coda limitata (bounded queue) thread-safe può essere costruita con una std::mutex, variabili di condizione, un contenitore a capacità fissa e un flag di chiusura (shutdown). L'operazione di push si blocca, va in timeout o fallisce quando la coda è piena, fornendo così un meccanismo di backpressure; l'operazione di pop si blocca quando la coda è vuota. Entrambe le operazioni dovrebbero attendere su predicati come size < capacity || closed e !empty || closed. In fase di spegnimento, risveglia i produttori e i consumatori bloccati; rifiuta nuovi inserimenti e definisci se i consumatori debbano svuotare gli elementi esistenti o fermarsi immediatamente. L'API dovrebbe specificare il comportamento bloccante, la semantica di chiusura, i valori di ritorno e le garanzie di concorrenza.

Prova a rispondere a questa domanda con un coach IA

Database

19Come si esegue una migrazione dello schema per un servizio C++ senza tempi di inattività (downtime)?

Usa una migrazione a fasi di tipo expand-contract. Per prima cosa, apporta aggiunte allo schema retrocompatibili, come colonne nullable o nuove tabelle, che non interrompano il servizio C++ attualmente in esecuzione. Distribuisci codice compatibile in grado di gestire sia le rappresentazioni vecchie che quelle nuove, esegui il backfill dei dati esistenti in piccoli lotti sottoposti a throttling, convalida la coerenza, sposta le letture sul nuovo schema e solo successivamente rimuovi le vecchie colonne o il vecchio codice una volta che tutte le versioni distribuite non ne dipendono più. Per tabelle di grandi dimensioni, evita lunghe istruzioni DDL (Data Definition Language) bloccanti, usa operazioni online/concorrenti dove supportate, monitora blocchi/replicazione/risorse e mantieni percorsi di rollback funzionanti con codice parzialmente distribuito e dati parzialmente migrati.

Prova a rispondere a questa domanda con un coach IA

20Come dovrebbe un servizio C++ leggere dalle repliche del database gestendo al contempo le letture non aggiornate (stale read) e le garanzie read-your-writes?

Tratta le letture dalle repliche come un compromesso esplicito sulla coerenza. Le letture che possono tollerare dati non aggiornati (staleness) possono essere indirizzate a repliche in salute, ma le letture con garanzia 'read-your-writes', transazionali o critiche per l'aggiornamento dei dati dovrebbero andare al nodo primario, a meno che non sia noto che la replica scelta abbia applicato la posizione di scrittura rilevante, come un LSN (Log Sequence Number), un timestamp o una versione. Monitora il ritardo (lag) e lo stato di salute delle repliche, codifica la coerenza richiesta per ogni endpoint o richiesta e usa il primario come fallback, attendi l'allineamento (catch-up) o fallisci quando le repliche superano il limite di staleness consentito. Documenta il modello di coerenza in modo che i chiamanti sappiano quali letture potrebbero non essere aggiornate.

Prova a rispondere a questa domanda con un coach IA