1Comment 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é.
2Expliquez 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.
3Expliquez 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.
4Comparez 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é.
5Comparez std::variant avec le polymorphisme basé sur l’héritage pour modéliser des messages ou événements hétérogènes.
std::variant est un type valeur qui contient exactement une alternative parmi un ensemble fixe et fermé de types, et se manipule couramment avec std::visit ou des requêtes de type explicites. Il est utile pour des messages de protocole ou des événements lorsque l’ensemble des types de messages est connu et que l’on veut une gestion sûre du point de vue des types, sans dispatch virtuel et souvent sans allocation sur le tas par objet. Le polymorphisme basé sur l’héritage utilise une classe de base et des fonctions virtuelles pour effectuer le dispatch via une interface commune ; il est préférable lorsque l’ensemble des types de messages dérivés est ouvert, extensible indépendamment, de type plugin, ou masqué derrière une interface stable. variant favorise les types somme fermés, la localité et la vérification à la compilation ; l’héritage favorise l’extensibilité, le polymorphisme à l’exécution et la conception basée sur des interfaces.
6Qu’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.
7Distinguez les comportements indéfinis, non spécifiés et définis par l’implémentation, avec des exemples pertinents pour le backend.
Un comportement indéfini signifie que la norme C++ n’impose aucune exigence ; les résultats peuvent inclure des plantages, une corruption de données, des failles de sécurité ou une mauvaise compilation dépendante de l’optimiseur. Les exemples incluent l’accès hors limites, l’utilisation après libération, le dépassement d’entier signé et les courses de données. Un comportement non spécifié signifie que la norme autorise plusieurs résultats et que l’implémentation n’a pas à documenter lequel est choisi ; un exemple courant est l’ordre d’évaluation de nombreux arguments de fonction, de sorte que le code ne doit pas dépendre de l’évaluation d’effets de bord dans un ordre particulier. Un comportement défini par l’implémentation signifie que l’implémentation doit choisir et documenter le comportement ; les exemples incluent le caractère signé ou non de `char` simple, les tailles/plages de certains types fondamentaux dans les limites de la norme, et certains comportements du décalage à droite signé. Dans le code backend, l’UB est un risque de correction et de sécurité, tandis que les comportements non spécifiés et définis par l’implémentation sont des risques de portabilité qui doivent être évités ou isolés dans les protocoles, le stockage et la logique multiplateforme.
8Expliquez les niveaux de sûreté vis-à-vis des exceptions et la manière dont `noexcept` affecte les opérations de déplacement, les conteneurs et la génération de code.
Les niveaux de sûreté vis-à-vis des exceptions décrivent ce qui reste vrai si une opération lance une exception. La garantie de base signifie que les invariants sont préservés et que les ressources ne fuient pas, même si l’état a pu changer. La garantie forte signifie une sémantique de validation ou retour arrière : en cas d’échec, l’état observable est inchangé. La garantie nothrow signifie que l’opération ne lance pas d’exception. Les techniques courantes incluent RAII, le travail sur des temporaires, copy-and-swap et l’ordonnancement des mutations de sorte que la validation ne se produise qu’après la réussite du travail susceptible de lancer. `noexcept` est un contrat : si une fonction `noexcept` lance, `std::terminate` est appelé. Il affecte aussi le code générique et les conteneurs : par exemple, pendant une réallocation, `std::vector` peut déplacer les éléments lorsque le constructeur de déplacement est `noexcept` ; sinon il peut copier, souvent via `std::move_if_noexcept`, afin de préserver les garanties d’exception. `noexcept` peut aussi aider la génération de code ou l’optimisation en réduisant les chemins nécessaires de propagation d’exceptions, mais uniquement lorsque la promesse est correcte.
9Expliquez `std::expected` ou les types outcome équivalents comme alternatives aux exceptions pour les opérations susceptibles d’échouer.
`std::expected<T, E>` représente soit une valeur de succès `T`, soit une erreur explicite `E`, ce qui intègre l’échec au type de retour de la fonction au lieu de s’appuyer sur la propagation d’exceptions. C’est utile pour les opérations susceptibles d’échouer où les erreurs sont attendues et où les appelants doivent les gérer localement, comme l’analyse, la validation, les appels réseau, les recherches dans le stockage et les opérations backend réessayables. Le type d’erreur doit porter des informations structurées telles qu’un code d’erreur, une catégorie, la possibilité de réessayer, un message ou un mappage HTTP/RPC. Les types expected/outcome se composent en vérifiant et en propageant les erreurs et, dans les API de style C++23, peuvent utiliser des opérations monadiques comme `and_then`, `transform` et `or_else` pour enchaîner les étapes sans conditionnelles profondément imbriquées. Par rapport aux exceptions, ils rendent le flot de contrôle et les contrats d’API explicites et fonctionnent bien aux frontières sans exceptions ou d’ABI ; les exceptions peuvent toutefois rester appropriées pour des défaillances rares, non locales ou réellement exceptionnelles selon la politique du projet.
10Décrivez les règles de durée de vie des objets automatiques, dynamiques, temporaires et référencés de manière asynchrone.
Les objets automatiques vivent de leur construction jusqu’à la fin de leur portée ; les pointeurs ou références vers eux deviennent pendants après la sortie de cette portée. Les objets dynamiques vivent de leur allocation/construction jusqu’à ce qu’ils soient explicitement détruits ou qu’un objet propriétaire les détruise ; les pointeurs bruts et les références ne prolongent pas la durée de vie. Les temporaires vivent généralement jusqu’à la fin de l’expression complète (full expression), avec des règles spécifiques de prolongation de durée de vie lorsqu’ils sont liés à des références, mais pas dans tous les usages. Du travail asynchrone comme des callbacks, des threads, des timers ou des coroutines peut s’exécuter après la destruction de l’objet référencé ; les captures et références stockées nécessitent donc une gestion explicite de la durée de vie afin d’éviter les références pendantes et les use-after-free.
11Décrivez les problèmes d’ordre d’initialisation statique et la façon dont constinit, les magic statics et l’injection de dépendances les atténuent.
Les problèmes d’ordre d’initialisation statique surviennent parce que les objets à portée d’espace de noms ou static initialisés dynamiquement dans différentes unités de traduction ont un ordre relatif d’initialisation non spécifié. Un global peut en utiliser un autre avant qu’il n’ait été construit ; l’ordre de destruction peut aussi créer des problèmes similaires à l’arrêt du programme. constinit force une variable static ou thread-local à avoir une initialisation statique/constante, sinon le programme est mal formé, ce qui évite une dépendance à l’ordre d’initialisation dynamique pour cette variable. Les magic statics, ou variables static locales à une fonction, sont initialisées à la première utilisation et sont thread-safe depuis C++11. L’injection de dépendances évite les dépendances globales cachées en construisant les objets dans un ordre contrôlé et en passant les dépendances explicitement.
12Que sont l’élision de copie, la NRVO et la RVO, et quand peut-on s’y fier ?
L’élision de copie consiste à construire un objet directement à sa destination finale au lieu de créer un temporaire séparé puis de le copier ou de le déplacer. RVO désigne généralement le retour d’un temporaire non nommé ou d’une prvalue, par exemple return T{}; en C++17 et versions ultérieures, beaucoup de ces cas sont obligatoires parce que la prvalue initialise directement l’objet résultat. NRVO consiste à retourner un objet local nommé, par exemple return x; le compilateur est autorisé à construire ce local directement dans l’emplacement de retour de l’appelant, mais ce n’est pas garanti. On peut se fier aux cas d’élision obligatoire de prvalue en C++17 spécifiés, mais pas au fait que la NRVO se produise toujours ; évitez std::move sur une valeur locale nommée retournée, car cela peut empêcher la NRVO.
13Comment les exigences relatives aux comparateurs, à l’égalité et aux fonctions de hachage affectent-elles la correction des conteneurs associatifs ?
Les conteneurs associatifs s’appuient sur la comparaison, l’égalité et le hachage pour définir l’identité des clés et maintenir leurs invariants internes. Les conteneurs ordonnés comme std::map exigent que le comparateur impose un ordre faible strict ; les clés sont considérées comme équivalentes lorsqu’aucune n’est inférieure à l’autre, et pas nécessairement selon operator==. Les conteneurs non ordonnés exigent que l’égalité soit une relation d’équivalence, et que deux clés considérées égales produisent la même valeur de hachage. Les clés ne doivent pas être modifiées pendant qu’elles sont stockées d’une manière qui change leur ordre, leur égalité ou leur hachage, car cela peut rendre incorrects les comportements de recherche, de suppression et d’unicité.
14Expliquez la recherche hétérogène dans les conteneurs associatifs ordonnés et non ordonnés.
La recherche hétérogène permet de rechercher dans un conteneur associatif avec un type différent du type de clé, en évitant la construction d’une clé temporaire. Dans les conteneurs ordonnés, cela exige un comparateur transparent, par exemple std::less<> ou un comparateur personnalisé avec un marqueur is_transparent. Dans les conteneurs non ordonnés, cela exige à la fois des foncteurs de hachage et d’égalité transparents capables de gérer le type de clé et le type de recherche de manière cohérente. Un exemple backend courant est un conteneur indexé par std::string qui peut être recherché avec std::string_view ou const char* sans allouer de std::string temporaire.
15Expliquez le modèle mémoire de C++ : data races, happens-before et garanties de synchronisation.
Le modèle mémoire de C++ définit quand les opérations dans différents threads sont ordonnées et quand les écritures deviennent visibles. Une data race se produit lorsque deux threads accèdent simultanément au même emplacement mémoire, qu’au moins un accès est une écriture et que les accès ne sont pas ordonnés par happens-before ni rendus sûrs autrement par des opérations atomiques ; une data race provoque un comportement indéfini. Happens-before est la relation d’ordre qui rend les effets de bord antérieurs visibles aux opérations ultérieures. Les opérations de synchronisation créent cet ordre : par exemple, le déverrouillage d’un mutex synchronizes-with un verrouillage réussi ultérieur du même mutex, et des opérations atomiques release/acquire appropriées peuvent synchroniser des threads. Les programmes corrects utilisent des mutexes, des atomiques ou d’autres mécanismes de synchronisation pour établir happens-before pour l’état partagé.