Test technique C++ : expliquer la mémoire, RAII et les erreurs de durée de vie

Préparez un test technique C++ : corrigez une vue sur un tampon détruit, suivez RAII et move, puis expliquez une preuve AddressSanitizer et ses limites.

Author: PracHub

Published: 10/11/2026

Test technique C++ : expliquer la mémoire, RAII et les erreurs de durée de vie

October 11, 2026

Quick Overview

Un exercice C++20 original suit un tampon de capteur, une vue span et leur durée de vie. Reproduisez un accès après libération avec AddressSanitizer, puis expliquez la correction, le transfert unique_ptr et le nettoyage d’un membre quand le constructeur extérieur échoue. Dix assertions natives bornent les conclusions.

Software EngineerFree

Pour expliquer un défaut mémoire en test technique C++, identifiez le propriétaire d’une ressource et la durée pendant laquelle les autres objets peuvent l’utiliser. Avant de réciter RAII ou les smart pointers, montrez quelle destruction rend un accès invalide et quelle correction change ce contrat.

Ce guide propose un exercice original de tampon de capteur : une fonction renvoie une vue sur des lectures qui ne vivent pas assez longtemps. Corrigez le retour, suivez un transfert de propriété et examinez un constructeur qui échoue. Pour prolonger ce raisonnement, travaillez Find and Fix C++ Ownership Bugs: Double Free, Shallow Copy, and Object Slicing.

Périmètre des preuves : les C++ Core Guidelines, le brouillon public du standard et la documentation Clang fondent les mécanismes cités. Le code et les données sont originaux. Dix assertions natives et un contre-exemple AddressSanitizer ont été exécutés en C++20 avec Apple Clang 21.0.0 sur macOS. Il n’y a ni capteur physique ni garantie de détection exhaustive. Aucun témoignage de candidat ne permet ici d’annoncer le format d’un test d’entreprise.

Un tampon de capteur possède les lectures tandis qu’une vue les emprunte pendant une durée limitée

Exercice : une vue ne conserve pas son propriétaire

La fonction suivante produit trois lectures entières fictives. Le type de retour semble pratique : il permet au consommateur de parcourir les valeurs sans posséder un autre conteneur. Avant d’exécuter, indiquez quel objet possède les éléments et à quel moment il est détruit.

std::span<const int> read_bad() {
    std::vector<int> values{12, 15, 18};
    return values;
}

Le propriétaire des éléments est le std::vector local. Le span représente une vue sur sa séquence ; il n’en prolonge pas la durée de vie. À la sortie de la fonction, le vecteur est détruit et son stockage libéré. Le span retourné contient encore une description de cette ancienne zone, mais elle n’est plus utilisable pour lire les éléments.

Le brouillon public du standard sur span le décrit comme une vue sur une séquence contiguë dont un autre objet possède le stockage. Nous utilisons ici la version C++20. Le brouillon courant peut contenir des évolutions plus récentes ; le comportement étudié ne dépend pas d’une nouvelle méthode de vérification de bornes.

Le const interdit la modification des entiers par cette vue. Il ne protège pas leur durée de vie. Copier le span copie la vue ; cela ne copie pas les lectures. Le placer dans une autre structure ou le capturer dans une lambda ne répare donc pas le problème. Il faut changer le propriétaire ou limiter l’usage de la vue à une portée valide.

Ne prédisez pas un affichage stable de 12. L’accès au premier élément après retour a un comportement indéfini. Il peut sembler fonctionner dans un exécutable donné, puis changer avec l’optimisation ou l’environnement. Le raisonnement sur la destruction suffit à identifier le défaut ; une sortie apparemment correcte ne constitue pas une réfutation.

Lire la preuve AddressSanitizer sans généraliser

Dans notre exécutable fautif, main appelle read_bad, puis lit le premier élément du span. La compilation instrumentée utilise C++20, -O0, -g, -fsanitize=address et -fno-omit-frame-pointer. Le processus se termine avec un code non nul et un rapport contenant heap-use-after-free.

La documentation AddressSanitizer de Clang explique l’instrumentation et les catégories d’erreurs détectables. Pour ce cas, l’information utile relie l’accès, la libération et l’allocation du stockage. Le nom de l’erreur indique un accès après libération sur le tas ; il ne signifie pas que l’objet vector lui-même devait être alloué avec new.

Le vecteur local et le tableau dynamique qu’il gère ont des stockages distincts dans notre exécution. Dans cette implémentation, le conteneur local gère une allocation dynamique pour les lectures. Sa durée de vie se termine à la sortie de fonction, ce qui libère cette allocation. Dire seulement « variable sur la pile » ne décrit pas toute la chaîne de propriété.

Conservez le rapport, les options de compilation et la version du compilateur. Les adresses et la présentation exacte peuvent varier entre machines ; expliquez la relation entre accès et libération. Expliquez plutôt la relation qui reste pertinente : l’accès survient après la libération du stockage que le span désigne.

Le passage de la version corrigée sous AddressSanitizer ne prouve pas l’absence de tout défaut mémoire. Il couvre les chemins exécutés avec ce jeu de données. Il ne teste pas la concurrence, un pilote, des entrées non fournies ni tous les mécanismes d’invalidation possibles. Utilisez l’outil comme une observation supplémentaire, avec une explication du contrat.

Corriger le retour avec un propriétaire explicite

Le contrat de cet exercice exige que l’appelant conserve le lot après le retour. La correction choisie renvoie donc le conteneur propriétaire. Une fonction séparée emprunte ensuite une vue pendant le calcul ; elle ne la stocke pas pour plus tard.

std::vector<int> read_samples() {
    return {12, 15, 18};
}
int total(std::span<const int> view) {
    return std::accumulate(view.begin(), view.end(), 0);
}

L’appelant crée owner = read_samples(), puis construit un span à partir de owner et appelle total. Deux assertions vérifient les trois lectures et leur somme de 45. Une vue vide produit zéro pour ce calcul. Le propriétaire reste vivant pendant l’utilisation, ce qui élimine le défaut précis du retour d’une vue sur un vecteur local détruit.

Le retour par valeur ne signifie pas qu’il faut écrire une copie manuelle du tampon. Le langage et les conteneurs disposent de mécanismes de construction et de déplacement ; discutez leurs coûts avec le code et les types concernés. Ici, aucune mesure ne justifie d’abandonner la propriété explicite pour économiser un coût supposé.

Une autre API pourrait demander à l’appelant de fournir un tampon, puis remplir ce tampon dans une limite annoncée. Elle aurait besoin d’un contrat sur la capacité et le nombre de lectures valides. Ce n’est pas la correction implémentée ici. En entretien, présentez une solution cohérente et ses contraintes avant d’énumérer des alternatives.

Si un traitement asynchrone devait conserver les lectures, un simple span emprunté demanderait que le propriétaire survive jusqu’à la fin réelle du traitement. Le retour d’un vecteur au premier appelant ne résout pas automatiquement cette extension. Identifiez alors le responsable du stockage et l’événement qui autorise sa destruction.

La vue fautive survit au vecteur local ; la correction conserve le propriétaire pendant le calcul

Expliquer RAII sur un chemin qui échoue

Les C++ Core Guidelines recommandent de gérer les ressources par des objets propriétaires, avec acquisition et libération liées à leur durée de vie. Pour notre exercice, l’intérêt n’est pas d’ajouter delete à chaque branche ; il est d’exprimer une propriété qui continue à fonctionner quand une validation échoue.

La fixture suivante utilise Probe pour compter acquisitions et libérations. Ce n’est pas une connexion réelle à un capteur. Le membre resource est construit avant l’exécution du corps du constructeur, qui peut ensuite refuser la calibration.

struct SensorSession {
    std::unique_ptr<Probe> resource = std::make_unique<Probe>();
    static inline int outer_destructors = 0;
    explicit SensorSession(bool reject) {
        if (reject) throw std::runtime_error("invalid calibration");
    }
    ~SensorSession() { ++outer_destructors; }
};

Si la validation lève une exception, la construction de SensorSession n’aboutit pas. Son destructeur n’est pas exécuté comme celui d’un objet complètement construit. En revanche, ses sous-objets déjà construits sont nettoyés lors du déroulement de pile. Le brouillon du standard sur les exceptions de construction décrit ce mécanisme.

Dans notre test, le compteur du destructeur extérieur reste à zéro pour l’objet refusé, tandis que le Probe acquis est libéré par le membre propriétaire. Sur le chemin accepté, l’objet est construit, puis son destructeur extérieur et celui de son membre interviennent à la sortie de portée. Les compteurs distinguent ces deux chemins.

Cette vérification ne simule pas toutes les exceptions possibles. Si l’acquisition elle-même échoue avant de rendre une ressource, ou si le nettoyage d’un système externe a d’autres contraintes, il faut examiner ce contrat. Ne faites pas dépendre la sûreté d’un objet partiellement construit d’une liste de réparations écrites dans son destructeur extérieur.

La portée de RAII dépasse la mémoire : un verrou ou un descripteur peut avoir un propriétaire similaire. Cela ne rend pas toutes les opérations réversibles. Libérer une ressource locale ne retire pas une commande déjà envoyée au matériel. Notre fixture ne produit aucun effet externe, ce qui limite les conclusions possibles.

Suivre un transfert de propriété sans confondre move et copie

Le programme crée aussi un std::unique_ptr<Probe> nommé a, puis initialise b avec std::move(a). Le contrat de unique_ptr définit ce transfert. Dans ce cas, le test vérifie que a est vide, que b possède le Probe et que la sortie de portée le libère une fois.

std::move ne réalise pas à lui seul une opération magique de déplacement ; il permet ici la sélection du constructeur approprié. Le comportement dépend ensuite du type. Ne généralisez pas l’état vide de ce unique_ptr à tous les objets déplacés. Pour un autre type, consultez son contrat avant d’utiliser l’objet source.

Un pointeur brut ou une vue déjà obtenue ne devient pas automatiquement propriétaire parce qu’un autre objet a été déplacé. Son utilisation reste soumise à la durée de vie de la ressource désignée. Dessinez le propriétaire après transfert, puis demandez quand cette ressource sera détruite ; évitez de suivre seulement les noms des variables.

Le recours à shared_ptr doit correspondre à une vraie propriété partagée. Précisez malgré tout qui conserve le tampon et jusqu’à quel événement. Même avec une durée de vie prolongée, les règles de modification, de concurrence et d’invalidation restent à traiter. Notre correction simple a un seul propriétaire et des lectures synchrones empruntées.

Défendre les tests et leurs limites à l’oral

Observation nativeConclusion et limite
Ancien exécutable : heap-use-after-freeLa lecture étudiée utilise le stockage libéré ; pas de valeur de sortie définie
Lot propriétaire : taille 3, total 45Le calcul sur le lot fourni fonctionne pendant sa durée de vie
Vue vide : total 0Le cas vide est défini pour cette fonction de somme
Transfert unique_ptr : source vide, cible vivanteLe contrat de ce type est vérifié sur le transfert testé
Constructeur refusé : membre libéré, destructeur extérieur absentLe nettoyage des sous-objets construits fonctionne sur ce chemin
Version corrigée : dix assertions, sortie normale sous ASanLes chemins exécutés passent ; aucune preuve exhaustive de sûreté

Si le recruteur propose d’agrandir le vecteur après avoir créé une vue, examinez l’invalidation. Le contrat des modifications de vector précise notamment les effets d’une réallocation sur les références, pointeurs et itérateurs. Le lot de notre test reste inchangé pendant le calcul ; il ne couvre pas une vue conservée après croissance du conteneur.

Une réponse orale peut suivre trois phrases : « Le span emprunte les éléments du vecteur local. Le retour détruit ce propriétaire avant l’accès. Je renvoie un propriétaire et limite l’emprunt au calcul, puis je vérifie le contre-exemple et les chemins de nettoyage. » Ajoutez ensuite une limite demandée, comme le maintien d’une vue après mutation.

Choisir une pratique qui oblige à expliquer la propriété

Question PracHubTravail ciblé
Find and Fix C++ Ownership Bugs: Double Free, Shallow Copy, and Object SlicingDessinez le propriétaire et chaque accès emprunté avant la correction
Implement a Unique Pointer and VectorExpliquez acquisition, transfert et libération sans double propriétaire
Design a C++ Variant with Safe Object LifetimesDistinguez stockage disponible et objet dont la construction a abouti
Explain C++ virtual dispatch and object lifetimePrécisez quel destructeur intervient selon le contrat du type
Fix and harden an object poolIdentifiez les emprunts encore actifs lors de la réutilisation d’un emplacement

Ces tâches ne constituent pas un test officiel d’employeur. Commencez par Find and Fix C++ Ownership Bugs: Double Free, Shallow Copy, and Object Slicing, en conservant votre explication initiale, la preuve observée et les limites de la correction.

Sources and Further Reading


Comments (0)