1Expliquez RAII et la manière dont ce principe façonne la gestion des ressources sûre vis-à-vis des exceptions dans les services backend en C++.
RAII (Resource Acquisition Is Initialization) signifie qu’un objet C++ possède une ressource et la libère dans son destructeur. Comme les objets locaux sont détruits automatiquement lorsque leur durée de vie/portée se termine, y compris pendant le déroulement de pile dû à une exception, RAII fournit un nettoyage déterministe et rend les chemins d’erreur sûrs vis-à-vis des exceptions. Dans les services backend, cela s’applique non seulement à la mémoire, mais aussi aux descripteurs de fichiers, sockets, verrous de mutex, handles de base de données, transactions et autres ressources de l’OS ou de l’application.
2Comparez std::unique_ptr et std::shared_ptr et décrivez quand chacun est approprié dans les API backend.
std::unique_ptr représente une propriété exclusive : il est peu coûteux, déplaçable mais non copiable, et convient à un propriétaire unique ou aux API qui transfèrent la propriété. std::shared_ptr représente une propriété partagée : il est copiable et maintient un objet en vie par comptage de références jusqu’à ce que le dernier propriétaire fort le libère. Dans les API backend, utilisez unique_ptr lorsque la propriété est transférée, shared_ptr uniquement lorsque plusieurs propriétaires indépendants doivent prolonger la durée de vie, et des références ou des pointeurs bruts pour les accès non propriétaires. std::make_shared est couramment préféré lors de la création d’objets shared_ptr, car il est efficace et sûr vis-à-vis des exceptions.
3Comment fonctionnent les deleters personnalisés dans les pointeurs intelligents, et quand sont-ils utiles pour l’interopérabilité C/C++ ?
Un deleter personnalisé est une logique de nettoyage appelable utilisée par un pointeur intelligent à la place de l’opération delete par défaut lorsque le pointeur libère la ressource qu’il possède. Il est utile pour l’interopérabilité C/C++ lorsqu’une ressource doit être libérée avec une fonction spécifique comme fclose, close, curl_easy_cleanup, SSL_free, free ou une fonction de destruction de bibliothèque. Pour unique_ptr, le type du deleter fait partie du type de unique_ptr et peut affecter sa taille ; les deleters sans état peuvent être optimisés, tandis que les deleters pointeurs de fonction ou avec état ajoutent du stockage. Pour shared_ptr, le deleter est stocké dans le bloc de contrôle et s’exécute lorsque le dernier propriétaire fort libère l’objet. Les deleters personnalisés permettent aux ressources C de participer à RAII en toute sûreté.
4Expliquez weak_ptr et la mécanique du bloc de contrôle de shared_ptr, notamment les cycles, enable_shared_from_this et les coûts de comptage de références.
Un shared_ptr gère la propriété partagée au moyen d’un bloc de contrôle contenant les compteurs de références fortes et faibles ainsi que les informations de nettoyage comme le deleter et l’allocateur. Copier ou détruire des objets shared_ptr incrémente ou décrémente le compteur fort, généralement avec des opérations atomiques ; des objets shared_ptr distincts peuvent donc être manipulés en toute sécurité entre threads, mais chaque mise à jour du compteur a un coût. Lorsque le compteur fort atteint zéro, l’objet géré est détruit ; le bloc de contrôle reste jusqu’à ce que les références faibles aient aussi disparu. weak_ptr pointe vers le même bloc de contrôle sans prolonger la durée de vie de l’objet ; lock() retourne un shared_ptr si l’objet est encore vivant et un shared_ptr vide sinon. Les cycles constitués uniquement de shared_ptr fuient, car les compteurs forts n’atteignent jamais zéro ; weak_ptr est donc utilisé pour les pointeurs arrière ou les liens d’observation. enable_shared_from_this permet à un objet déjà possédé par shared_ptr de créer un nouveau shared_ptr vers lui-même en utilisant le bloc de contrôle existant, évitant des blocs de contrôle séparés dangereux. Les coûts de comptage de références comprennent les incréments/décréments atomiques, la contention de cache, l’allocation du bloc de contrôle et le surcoût dans les chemins critiques.
5Qu’est-ce que la sémantique de déplacement, et comment implémente-t-on correctement la construction par déplacement et l’affectation par déplacement ?
La sémantique de déplacement permet à C++ de transférer des ressources depuis des objets temporaires ou autrement sacrifiables au lieu de les copier. Elle utilise des références rvalue comme T&& et std::move, qui est un cast permettant de sélectionner les surcharges de déplacement ; std::move ne déplace rien par lui-même. Un constructeur par déplacement correct initialise un nouvel objet en prenant la ressource de l’objet source et en laissant la source valide, destructible et assignable. Un opérateur d’affectation par déplacement correct transfère vers un objet existant, gère ou tolère l’auto-affectation, libère ou réutilise la ressource courante de la destination, prend la ressource de la source et laisse la source dans un état sûr. Les opérations de déplacement doivent souvent être noexcept afin que les conteneurs standard puissent les utiliser lors des réallocations tout en préservant les garanties vis-à-vis des exceptions.
6Décrivez la const-correctness dans les API C++ et comment concevoir efficacement des fonctions membres const.
La const-correctness consiste à exprimer via le système de types quelles opérations ne modifient pas l’état observable ou logique d’un objet. Une fonction membre const possède un objet this qualifié const, elle ne peut donc pas modifier les membres de données non mutable ni appeler des fonctions membres non const sur le même objet. Une bonne conception d’API marque les requêtes en lecture seule comme const, renvoie des valeurs ou des références/pointeurs const lorsque c’est approprié, et évite d’exposer un état interne mutable depuis des fonctions const. mutable doit être réservé aux détails d’implémentation qui ne changent pas l’état logique, comme les caches, les valeurs calculées paresseusement, les métriques ou les mutex. const est un contrat d’API sur la mutation, pas une garantie automatique de sûreté vis-à-vis des threads ; les garanties de concurrence nécessitent une implémentation et une documentation séparées.
7Que sont les types move-only et comment influencent-ils les frontières d’API pour les sockets, les handles de fichiers et les verrous ?
Les types move-only sont des types qui ne peuvent pas être copiés mais peuvent être déplacés. Ils sont courants pour la propriété unique de ressources telles que les sockets, les descripteurs de fichiers, les handles de fichiers, les verrous et `std::unique_ptr`. Les opérations de copie sont supprimées pour éviter la duplication de propriété et la double libération ; les opérations de déplacement transfèrent la propriété et laissent la source valide, mais généralement vide ou non propriétaire. Les API doivent rendre explicites les frontières de propriété : les factories peuvent renvoyer des objets move-only par valeur, les fonctions qui prennent possession peuvent accepter par valeur ou par référence rvalue, et les fonctions qui inspectent seulement doivent prendre des références, des pointeurs ou d’autres handles empruntés. Les conteneurs peuvent stocker des valeurs move-only lorsque les éléments sont insérés ou relocalisés par déplacement.
8Comparez auto, decltype, decltype(auto) et la déduction d’arguments de template dans du code backend courant.
auto utilise une déduction de type proche de celle des templates pour les variables : un auto simple supprime généralement les références et le const de premier niveau sauf si la déclaration les demande, par exemple auto&, const auto& ou auto&&. decltype(expr) inspecte plus exactement le type déclaré ou le type de l’expression : une id-expression non parenthésée donne le type déclaré, tandis que les autres expressions lvalue produisent T&, les xvalues produisent T&& et les prvalues produisent T. decltype(auto) déduit en utilisant les règles de decltype, souvent pour les types de retour lorsque les références doivent être préservées. La déduction d’arguments de template est similaire à auto, mais dépend de la forme du paramètre, comme T, T&, const T& ou T&&, et possède ses propres règles. Les initialiseurs entre accolades constituent une différence fréquente : auto x = {1,2} déduit std::initializer_list<int>, tandis qu’un paramètre de template simple ne peut généralement pas déduire T à partir d’un initialiseur nu entre accolades, sauf si le paramètre attend un initializer_list ou un autre type approprié.
9Décrivez la règle de zéro, la règle de trois et la règle de cinq, ainsi que les situations où chacune s’applique.
Règle de zéro : préférer les classes qui ne déclarent pas d’opérations personnalisées de destruction/copie/déplacement ; laisser les membres RAII tels que std::string, std::vector, std::unique_ptr, les wrappers de fichiers/sockets, etc., gérer les ressources. Règle de trois : si une classe gère manuellement une ressource et a besoin d’un destructeur personnalisé, d’un constructeur de copie ou d’un opérateur d’affectation par copie, elle a généralement besoin des trois pour définir un comportement correct de copie/propriété. Règle de cinq : en C++11 et versions ultérieures, ces types doivent aussi considérer le constructeur de déplacement et l’opérateur d’affectation par déplacement. Utilisez la règle de zéro pour la plupart des types applicatifs ; utilisez la règle de trois/cinq lorsque le type possède directement une ressource ou a une sémantique non triviale de propriété/durée de vie.
10Décrivez les formes d’initialisation en C++ et les pièges courants, notamment `initializer_list` et les agrégats.
C++ possède plusieurs formes d’initialisation. L’initialisation par défaut, comme `T x;`, appelle un constructeur par défaut pour les types classe, mais laisse les variables automatiques fondamentales non initialisées. L’initialisation par valeur, comme `T x{};` ou `T()`, effectue une initialisation à zéro lorsque c’est applicable avant l’initialisation par constructeur ou par membre. L’initialisation par liste utilise des accolades, rejette les conversions rétrécissantes et possède des règles particulières de résolution de surcharge, notamment une forte préférence pour les constructeurs `std::initializer_list` viables. L’initialisation d’agrégat initialise directement les membres d’un agrégat avec des accolades ; C++20 prend aussi en charge les initialiseurs désignés pour les agrégats, dans l’ordre de déclaration. Les pièges courants incluent les scalaires locaux non initialisés, la sélection surprenante de surcharges `initializer_list`, les erreurs de rétrécissement avec les accolades, le most-vexing parse avec les parenthèses et les changements de comportement lorsqu’un type cesse d’être un agrégat.
11Qu’est-ce que le comportement indéfini en C++, et comment peut-il apparaître dans des incidents de production backend ?
Le comportement indéfini est un comportement pour lequel la norme C++ n’impose aucune exigence après qu’une opération invalide s’est produite. Le programme peut sembler fonctionner, planter, corrompre des données, exposer des failles de sécurité ou être optimisé en un comportement surprenant. Les compilateurs supposent que le comportement indéfini n’arrive pas et optimisent sur la base de cette hypothèse, si bien que les problèmes peuvent n’apparaître qu’en builds de release ou sous trafic de production. Les incidents backend peuvent provenir de pointeurs/références pendants, de use-after-free, de violations de durée de vie des objets, d’accès hors limites, de débordement d’entier signé, de data races, de casts invalides, de lectures non initialisées, de doubles libérations ou de violations de strict aliasing. Les mesures d’atténuation incluent RAII et une conception claire de la propriété/durée de vie, des abstractions plus sûres et des vérifications de bornes, les tests/le fuzzing, les revues de code, l’analyse statique et des sanitizers tels que ASan, UBSan et TSan.
12Expliquez les fonctions virtuelles, les vtables, le coût du dispatch dynamique, le slicing d’objet et les dangers liés aux destructeurs virtuels.
Une fonction virtuelle permet le polymorphisme à l’exécution : lorsqu’elle est appelée via un pointeur ou une référence de base, l’implémentation correspondant au type dynamique de l’objet est sélectionnée. La plupart des implémentations stockent un vptr caché dans chaque objet polymorphe, pointant vers une vtable d’adresses de fonctions virtuelles pour ce type dynamique. Le coût typique est un pointeur supplémentaire dans l’objet, un appel indirect, un impact possible sur le cache/la prédiction de branchement et moins d’occasions d’inlining, même si les compilateurs peuvent parfois dévirtualiser. Le slicing d’objet se produit lorsqu’un objet dérivé est copié ou stocké par valeur comme un objet de base, ce qui perd la partie dérivée et le comportement dynamique. Si une classe de base est destinée à être supprimée via un pointeur de base, son destructeur doit être virtuel ; sinon, supprimer un objet dérivé via ce pointeur de base a un comportement indéfini.
13Décrivez les horloges, les points temporels et les durées de std::chrono pour les délais d’expiration de service, les métriques et les horodatages.
`std::chrono` modélise le temps avec des horloges, des `time_point` et des `duration`. Une `duration` est un intervalle avec une unité, comme des millisecondes ou des secondes. Un `time_point` est un point sur la ligne temporelle d’une horloge spécifique. `steady_clock` est monotone et doit être utilisée pour le temps écoulé, les délais d’expiration de service, les échéances et les mesures de latence, car elle n’est pas affectée par les changements de l’horloge murale. `system_clock` représente le temps civil/mural et convient aux horodatages, à la journalisation, à la persistance et aux conversions calendaires, mais elle peut sauter lorsque l’heure système est ajustée. Les délais d’expiration doivent généralement être basés sur `steady_clock::now() + duration` ; les horodatages machine doivent utiliser un format de temps mural clairement documenté, typiquement UTC aux frontières d’API ou de stockage.
14Décrivez std::format et les mécanismes de formatage modernes par rapport aux iostreams et aux API de style printf.
`std::format` est le mécanisme de formatage type-safe de C++20 inspiré de fmtlib. Il utilise des champs de remplacement `{}` et des spécifications de format pour produire du texte formaté sans la syntaxe d’insertion avec état des iostreams ni les varargs C de style `printf`. Comparé à `printf`, il évite de nombreux problèmes d’inadéquation format/type ; comparé aux iostreams, il est souvent plus clair et plus facile à composer. fmtlib est la bibliothèque largement utilisée qui a précédé et influencé `std::format`, et peut offrir un support plus large ou des fonctionnalités plus récentes. Les types définis par l’utilisateur peuvent être formatés via un support de formatters personnalisés. Dans le logging à fort volume, les performances dépendent de l’évitement du formatage, des conversions et des allocations inutiles, en particulier pour les niveaux de log désactivés ; les API qui reportent le formatage ou vérifient d’abord le niveau de log sont préférables.
15Expliquez les catégories de valeur en C++ et le transfert parfait, ainsi que leur importance pour des API génériques efficaces.
Les catégories de valeur en C++ décrivent les expressions : les lvalues ont une identité et peuvent être référencées après l’expression ; les prvalues sont des rvalues pures, comme de nombreuses temporaires/valeurs calculées ; les xvalues sont des objets en fin de vie dont les ressources peuvent être réutilisées. Le transfert parfait est la technique de templates consistant à prendre une référence de transfert, typiquement T&& lorsque T est déduit, et à transférer avec std::forward<T>(arg) afin de préserver la catégorie de valeur de l’appelant : les lvalues restent des lvalues et les rvalues restent des rvalues. Les règles de réduction de références rendent cela possible. C’est important pour les API backend génériques, car les wrappers, fabriques, répartiteurs et fonctions de style emplace peuvent éviter les copies inutiles et préserver la sélection de surcharge ainsi que le comportement de déplacement.
16Décrivez la prévention des deadlocks et les stratégies de verrouillage multi-mutex avec std::scoped_lock et std::lock.
On prévient les deadlocks en évitant les attentes circulaires : acquérir les verrous dans un ordre global cohérent lorsque c’est possible, ou acquérir plusieurs mutexes avec `std::lock`/`std::scoped_lock`, qui utilisent un algorithme d’évitement des deadlocks. `std::scoped_lock lock(a, b, ...)` est la forme RAII la plus simple pour verrouiller plusieurs mutexes et les déverrouiller automatiquement à la sortie de la portée. Avec `std::lock`, il faut d’abord verrouiller les mutexes, puis attacher des wrappers RAII avec `std::adopt_lock`, ou utiliser `std::unique_lock` avec `std::defer_lock`. Gardez les sections critiques courtes et évitez les opérations bloquantes ou les callbacks inconnus pendant que des verrous sont détenus ; l’évitement des deadlocks ne garantit pas automatiquement l’équité et ne prévient pas tous les scénarios de livelock/famine.
17Expliquez les schémas d’utilisation de condition_variable pour attendre des changements d’état.
Utilisez `std::condition_variable` pour attendre qu’un prédicat d’état protégé par un mutex change. L’état partagé est modifié en détenant le même mutex, puis `notify_one` ou `notify_all` réveille les threads en attente. Les threads en attente doivent utiliser `cv.wait(lock, predicate)` ou une boucle équivalente, car les réveils peuvent être intempestifs et les notifications ne sont pas mémorisées indépendamment de l’état du prédicat. `notify_one` réveille un thread en attente ; `notify_all` réveille tous les threads en attente et convient aux changements d’état diffusés à tous, comme un arrêt.
18Concevez une file bornée thread-safe en utilisant les primitives C++ standard et définissez les garanties de son API.
Une file bornée thread-safe peut être construite avec un `std::mutex`, des variables de condition, un conteneur de capacité fixe et un indicateur d’arrêt/fermeture. `push` bloque, expire ou échoue lorsque la file est pleine, ce qui fournit une contre-pression ; `pop` bloque lorsqu’elle est vide. Les deux opérations doivent attendre sur des prédicats comme `size < capacity || closed` et `!empty || closed`. Lors de l’arrêt/la fermeture, réveillez les producteurs et consommateurs bloqués ; rejetez les nouveaux `push` et définissez si les consommateurs drainent les éléments existants ou s’arrêtent immédiatement. L’API doit préciser le comportement bloquant, la sémantique de fermeture, les valeurs de retour et les garanties de concurrence.
19Comment exécutez-vous une migration de schéma pour un service C++ sans interruption de service ?
Utilisez une migration expand-contract par étapes. Commencez par des ajouts de schéma rétrocompatibles, comme des colonnes nullable ou de nouvelles tables, qui ne cassent pas le service C++ actuellement en cours d'exécution. Déployez un code compatible capable de gérer à la fois les anciennes et les nouvelles représentations, rebouchez (backfill) les données existantes par petits lots bridés, validez la cohérence, basculez les lectures vers le nouveau schéma, et ne supprimez les anciennes colonnes ou le vieux code que plus tard, une fois que toutes les versions déployées n'en dépendent plus. Pour les grandes tables, évitez les DDL bloquants longs, utilisez des opérations en ligne/concurrentes lorsque c'est pris en charge, surveillez les verrous/la réplication/les ressources, et conservez des chemins de retour arrière qui fonctionnent avec un code partiellement déployé et des données partiellement migrées.
20Comment un service C++ devrait-il lire depuis des réplicas de base de données tout en gérant les lectures périmées et les garanties read-your-writes ?
Traitez les lectures sur réplica comme un compromis de cohérence explicite. Les lectures qui peuvent tolérer un décalage peuvent aller vers des réplicas sains, mais les lectures read-your-writes, transactionnelles ou critiques en fraîcheur doivent aller vers le primary, sauf si le réplica choisi est connu pour avoir rejoué la position d'écriture pertinente, par exemple un LSN, un horodatage ou une version. Suivez le lag et la santé des réplicas, encodez la cohérence requise par endpoint ou par requête, et basculez vers le primary, attendez le rattrapage, ou échouez lorsque les réplicas dépassent le décalage autorisé. Documentez le modèle de cohérence pour que les appelants sachent quelles lectures peuvent être périmées.