IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Mamadou Babaei : « C++ n'a pas besoin d'une nounou comme le vérificateur d'emprunts de Rust »
Faut-il vraiment changer de langage, ou simplement mieux maîtriser les outils existants ?

Le , par Stéphane le calme

36PARTAGES

11  0 
Alors que Rust s’impose comme le champion de la sécurité mémoire grâce à son système de vérification à la compilation, certains développeurs C++ refusent de se laisser reléguer au rang de programmeurs imprudents. Avec un ton provocateur, le développeur professionnel de jeu Mamadou Babaei défend la capacité du C++ à détecter et corriger les fuites mémoire sans avoir besoin d’un « gardien de mémoire » comme Rust. À travers une démonstration concrète avec _CrtDumpMemoryLeaks, il relance le débat : faut-il vraiment changer de langage, ou simplement mieux maîtriser les outils existants ?

Rust : un langage trop protecteur ?

Rust a été acclamé ces dernières années comme l’un des langages les plus sécurisés pour le développement système. Sa principale innovation ? Le borrow checker (littéralement vérificateur d'emprunts), ce mécanisme de vérification à la compilation qui empêche les erreurs de mémoire comme les accès concurrents non sécurisés ou les use-after-free.

Le vérificateur d'emprunts est une fonctionnalité essentielle du langage Rust et fait partie de ce qui rend Rust Rust. Il vous aide (ou vous oblige) à gérer la propriété. Comme le dit le chapitre 4 de « The Rust Programming Language », « La propriété est la caractéristique la plus unique de Rust, et elle permet à Rust de garantir la sécurité de la mémoire sans avoir besoin d'un collecteur mémoire ».

Le vérificateur d'emprunt est un composant du compilateur Rust dont le rôle est de vérifier à la compilation que l’usage de la mémoire est sûr, sans fuite ni corruption potentielle, sans exécution du programme. Il repose sur trois concepts clés :
  • Propriété (ownership) : chaque valeur a un propriétaire unique.
  • Emprunts (borrowing) : on peut prêter une valeur de manière mutable ou non mutable, mais pas les deux en même temps.
  • Durée de vie (lifetimes) : chaque référence a une durée de vie clairement définie et vérifiée.

Le borrow checker interdit à la compilation :
  • Les accès concurrents à une même valeur mutable (race conditions évitées).
  • Les références à des valeurs qui ont été libérées (dangling pointers).
  • La modification de données pendant qu'elles sont empruntées de façon immuable.
  • L’utilisation après déplacement (move), pour éviter la double libération.

Mais pour certains développeurs C++, ce système ressemble davantage à une « nounou » : une surprotection qui entrave la liberté de coder rapidement et efficacement. Selon eux, les développeurs expérimentés n’ont pas besoin d’un langage qui leur interdit de faire certaines choses – ils veulent juste des outils qui les aident à le faire mieux, sans être bridés.

Un faux procès fait au C++ ?

Depuis l’essor de Rust dans les milieux de la sécurité et du développement système, une idée reçue semble s’installer : le C++ serait intrinsèquement dangereux. Ses critiques mettent en avant la complexité de sa gestion manuelle de la mémoire, source supposée d’erreurs fréquentes et de vulnérabilités critiques.

Mais ce discours occulte une réalité plus nuancée : le C++ n’est pas dangereux par nature, il est exigeant. Comme le rappelle Babaei, les fuites mémoire et les erreurs d’allocation ne sont pas une fatalité si les développeurs exploitent les outils disponibles dans l’écosystème du langage.

Ainsi, plutôt que de dénigrer le C++ pour son absence de mécanismes de sécurité intégrés à la Rust, il conviendrait de promouvoir les bonnes pratiques de développement, les outils de débogage avancés et les bibliothèques modernes comme RAII, unique_ptr, shared_ptr, ou encore Valgrind.

Dans sa vidéo intitulée « Rust Devs Think We’re Hopeless; Let’s Prove Them Wrong (with C++ Memory Leaks)! », Mamadou Babaei défend avec humour et technicité la capacité des développeurs C++ à gérer efficacement la mémoire, sans recourir aux mécanismes de sécurité stricts de Rust. Il démontre comment détecter les fuites mémoire en C++ en utilisant l'outil _CrtDumpMemoryLeaks fourni par la bibliothèque d'exécution C (CRT) de Microsoft.

Et d'indiquer :

« Lorsque les développeurs Rust pensent à nous, les gens du C++, ils imaginent une lignée maudite - un traumatisme générationnel transmis de malloc à free. Pour eux, chaque ligne de C++ que nous écrivons est comme jouer à la roulette russe - sauf que les six chambres sont chargées de comportements non définis.

« Ils nous regardent comme si nous étions sans espoir. Comme si nous étions à deux doigts d'une thérapie. Mais vous savez quoi ? Nous n'avons pas besoin d'une nounou pour le compilateur. Pas de vérificateur d'emprunts. Pas de durée de vie. Pas de modèles de propriété. Pas de magie noire. Même Valgrind n'est pas nécessaire. Juste des pointeurs bruts, de la détermination brute, et un peu de santé mentale douteuse.

« Dans cette vidéo, je vais donc vous montrer comment traquer les fuites de mémoire comme si vous étiez né avec un pointeur dans une main et un débogueur dans l'autre ».


_CrtDumpMemoryLeaks : Un outil à (re)découvrir

La démonstration de Babaei repose sur un outil méconnu mais redoutablement efficace : la fonction _CrtDumpMemoryLeaks, intégrée à la CRT (C Run-Time Library) de Microsoft. Elle permet, dès la compilation en mode debug, de détecter automatiquement les blocs de mémoire non libérés avant la fin de l’exécution.

En couplant cet outil à des macros de suivi (_CRTDBG_MAP_ALLOC, new redéfini avec traçabilité), il devient possible de cartographier précisément l'origine d'une fuite mémoire. Cette démarche, bien que nécessitant une configuration spécifique, prouve que le C++ n’est pas dépourvu de garde-fous — il demande juste qu’on les active.

Démonstration : détection de fuites mémoire avec _CrtDumpMemoryLeaks

Babaei présente un exemple de programme C++ qui génère intentionnellement une fuite mémoire en allouant de la mémoire sans la libérer. Pour détecter cette fuite, il utilise les fonctions de débogage de la CRT.

Démonstration d'une fuite de mémoire en C++ simple et intentionnelle

Code C++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/** 
* Une fuite de mémoire en C++ simple et intentionnelle 
*/ 
  
#include <atomic> 
#include <cstdint> 
#include <cstdlib> 
#include <ctime> 
#include <iostream> 
#include <string> 
#include <thread> 
  
static constexpr int32_t PASSWORD_LENGTH = 1024 * 1024; 
static constexpr std::chrono::milliseconds INTERVAL_MILLISECONDS{1}; 
  
std::string GeneratePassword(std::size_t length) { 
    const std::string charset{ 
        "0123456789" 
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 
        "abcdefghijklmnopqrstuvwxyz" 
    }; 
  
    const std::size_t maxIndex = charset.size(); 
    std::string result(length, '\0'); 
  
    for (std::size_t i = 0; i < length; ++i) { 
        result[i] = charset[rand() % maxIndex]; 
    } 
  
    return result; 
} 
  
int32_t main() { 
    std::srand(static_cast<uint32_t>(std::time(nullptr))); 
    std::atomic<bool> stop{false}; 
  
    std::thread inputThread([&stop]() { 
        std::cin.get(); 
        stop = true; 
        }); 
  
    while (!stop) { 
        std::string* password{new std::string(GeneratePassword(PASSWORD_LENGTH))}; 
  
        std::cout 
            << "Generated password:" 
            << "\n" 
            << *password 
            << "\n\n" 
            << "Press the 'Enter' key to interrupt!" 
            << std::endl; 
  
        std::this_thread::sleep_for(INTERVAL_MILLISECONDS); 
    } 
  
    inputThread.join(); 
    std::cout << "'Enter' key detected! Will stop!" << std::endl; 
  
    return 0; 
}

Une démonstration de _CrtDumpMemoryLeaks

Code C++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/** 
* Une démonstration de _CrtDumpMemoryLeaks 
*/ 
#define MEMORY_LEAK_TRACKER 1 
  
#if MEMORY_LEAK_TRACKER 
#define _CRTDBG_MAP_ALLOC 
#include <crtdbg.h> 
  
#define DEBUG_NEW new (_NORMAL_BLOCK, __FILE__, __LINE__) 
#define new DEBUG_NEW 
#endif  /* MEMORY_LEAK_TRACKER */ 
  
#include <atomic> 
#include <cstdint> 
#include <cstdlib> 
#include <ctime> 
#include <iostream> 
#include <string> 
#include <thread> 
  
static constexpr int32_t PASSWORD_LENGTH = 64; 
static constexpr std::chrono::milliseconds INTERVAL_MILLISECONDS{1000}; 
  
std::string GeneratePassword(std::size_t length) { 
    const std::string charset{ 
        "0123456789" 
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 
        "abcdefghijklmnopqrstuvwxyz" 
    }; 
  
    const std::size_t maxIndex = charset.size(); 
    std::string result(length, '\0'); 
  
    for (std::size_t i = 0; i < length; ++i) { 
        result[i] = charset[rand() % maxIndex]; 
    } 
  
    return result; 
} 
  
int32_t main() { 
#if MEMORY_LEAK_TRACKER 
    int dbgFlags = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); 
    dbgFlags |= _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF; 
    _CrtSetDbgFlag(dbgFlags); 
  
    // Only track one allocation per run! 
    // e.g. allocation number that causes leaks: 
    //_CrtSetBreakAlloc(162); 
#endif  /* MEMORY_LEAK_TRACKER */ 
  
    std::srand(static_cast<uint32_t>(std::time(nullptr))); 
    std::atomic<bool> stop{false}; 
  
    std::thread inputThread([&stop]() { 
        std::cin.get(); 
        stop = true; 
        }); 
  
    while (!stop) { 
        std::string* password{new std::string(GeneratePassword(PASSWORD_LENGTH))}; 
  
        std::cout 
            << "Generated password:" 
            << "\n" 
            << *password 
            << "\n\n" 
            << "Press the 'Enter' key to interrupt!" 
            << std::endl; 
  
        std::this_thread::sleep_for(INTERVAL_MILLISECONDS); 
    } 
  
    inputThread.join(); 
    std::cout << "'Enter' key detected! Will stop!" << std::endl; 
  
#if MEMORY_LEAK_TRACKER 
    _CrtDumpMemoryLeaks(); 
#endif  /* MEMORY_LEAK_TRACKER */ 
  
    return 0; 
}

Explication

Code C++ : Sélectionner tout
1
2
3
4
5
6
7
8
9
#define MEMORY_LEAK_TRACKER 1 
  
#if MEMORY_LEAK_TRACKER 
#define _CRTDBG_MAP_ALLOC 
#include <crtdbg.h> 
  
#define DEBUG_NEW new (_NORMAL_BLOCK, __FILE__, __LINE__) 
#define new DEBUG_NEW 
#endif  /* MEMORY_LEAK_TRACKER */
Imaginons que nous construisions des maisons en LEGO et que nous oublions parfois de nettoyer les briques après la construction. Ce code dit : « Hé, activons un traqueur de briques LEGO ! ».

Il ajoute une caméra magique (<crtdbg.h>) qui surveille l'endroit où nous laissons tomber les briques. Et lorsque nous utilisons de nouvelles briques pour construire quelque chose, il ajoute secrètement qui l'a construit et où dans le code. Ainsi, si nous oublions de nettoyer, nous saurons exactement qui a fait le désordre et .

Explication technique :
  • _CrtSetDbgFlag(...) : Définit les drapeaux d'exécution pour activer le suivi des fuites de mémoire et le signalement automatique des fuites à la sortie du programme.
  • DBG_ALLOC_MEM_DF : Indique au tas de débogage de garder une trace des allocations de mémoire.
  • DBG_LEAK_CHECK_DF : Appelle automatiquement _CrtDumpMemoryLeaks() à la sortie de main().
  • _CrtSetBreakAlloc(162) : Fixe un point d'arrêt à la 162e allocation (telle qu'elle est comptée en interne par le tas CRT). Lorsqu'il est atteint, Visual Studio interrompt l'exécution afin que vous puissiez inspecter l'allocation à l'origine du problème. Vous pouvez commenter cette ligne sauf si vous essayez d'isoler une fuite spécifique dans un gros programme.

tl;dr ; _CrtSetDbgFlag(...) permet le suivi du tas au moment de l'exécution et les rapports de fuite ; _CrtSetBreakAlloc(N) vous permet de casser sur un numéro d'allocation spécifique.

Code C++ : Sélectionner tout
1
2
3
4
5
6
#if MEMORY_LEAK_TRACKER 
    _CrtDumpMemoryLeaks(); 
#endif  /* MEMORY_LEAK_TRACKER */ 
  
    return 0; 
}

[QUOTE]À la toute fin, nous disons à la caméra : « Ok, montre-moi tout ce qu'on a oublié de nettoyer. »

Elle envoie un rapport disant : « Hé ! Vous avez laissé une brique à la ligne 42 ! Et une autre à la ligne 57 ! »

Explication technique :
[LIST][*]Déclenche manuellement un rapport de fuite juste avant l'arrêt du programme.[*]C'est optionnel si vous avez déjà activé _CRTDBG_LEAK_CHECK_DF - le CRT va automatiquement faire le dumping des fuites.[*]Mais l'appeler[/*]...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.

Une erreur dans cette actualité ? Signalez-nous-la !

Avatar de koala01
Expert éminent sénior https://www.developpez.com
Le 12/05/2025 à 10:27
Salut,

Faut-il vraiment changer de langage, ou simplement mieux maîtriser les outils existants ?
Il faudrait surtout bien entraîner les développeurs débuttants
  • en leur faisant comprendre dés le début que ce qui peut échouer le fera obligatoirement
  • qu'on n'est pas dans un monde de bisounours et qu'ils doivent donc considérer toutes les ressources externes (introduction utilisateur, allocation de mémoire, accès réseaux ou fichiers) comme suspectes
  • en leur faisant comprendre qu'ils ne sont pas meilleurs que les utilisateurs lambda et qu'ils seront les premiers à faire des erreurs, mais que leurs erreurs peuvent mener à la catastrophe

Et, pour ce qui est spécifique au C++ en leur apprenant directement le langage de manière moderne, et non tel qu'on nous l'a appris à la fin du siècle dernier

Ce qui mènera forcément à la deuxième solution : une meilleure maîtrise des outils existants
Le borrow checker de Rust est-il un mal nécessaire ou une entrave à la créativité ?
Ce n'est sans doute pas une entrave à la créativité.

Par contre, c'est sûrement un mal rendu nécessaire par le manque de maîtrise des outils existants.

Cependant, ce n'est au final qu'un emplâtre sur une jambe de bois, car il ne résout pas le problème de fond : les gens manquent cruellement de l'entrainement nécessaire à l'utilisation correcte des outls existants.
Peut-on vraiment comparer deux langages à des stades aussi différents de maturité ?
Peut on seulement comparer deux langages de programmation quels qu'ils soient autrement que par leur philosophie ou par leur "performances brutes" (vitesse d'exécution, consommation de mémoire et consommation électrique)

Les outils de détection de fuites mémoire en C++ sont-ils suffisants pour garantir une gestion sûre de la mémoire dans des projets complexes ?
Très certainement.

Mieux encore, C++ dispose depuis près de quinze ans de nombreux outils capables de les éviter...

La complexité du système de propriété de Rust justifie-t-elle son adoption face à la flexibilité de C++ ?
Si cela peut rassurer les afficionados de Rust, pourquoi pas

Sans doute beaucoup moins pour ceux qui gardent en tête que tout se paye; ou que le fait de contourner un problème n'est pas forcément une solution à long terme
Rust est-il adapté aux débutants ou réservé aux ingénieurs systèmes expérimentés ?
C'est un très vaste débat, que l'on pourrait avoir à propos de nombreux langages de programmations sensés "simplifier le travail" du développeur.

Certains argueront que ces "facilités" permettent d'apprendre plus facilement le langage, qu'elles permettent d'obtenir plus facilement ou plus rapidement un résultat "correct et sécurisé".

D'autres prétendront qu'il ne sert à rien de faciliter le travail du dévoppeur si cela implique qu'il puisse ne pas maîtriser (voir qu'il puisse ne pas avoir conscience) des concepts de base du développement.

Les premiers répondront sans doute que les langages les plus simples sont particulièrement adpatés aux débuttants; les seconds répondront plutôt qu'ils ne devraient être utilisés que par des personnes chevronnées.

Encore une fois, il s'agit essentiellement d'une question de philosophie
6  1 
Avatar de smarties
Expert confirmé https://www.developpez.com
Le 12/05/2025 à 12:26
Je faisait un peu de C++ dans les années 2000 et maintenant je suis passé sur Rust.

Rust met beaucoup de chose en place pour facilité son adoption :
- une documentation centralisée
- un gestionnaire de dépendances
- le Rust book qui donne beaucoup d'éléments pour démarrer avec le langage dans de bonnes conditions
- des erreurs de compilations explicites
- cargo clippy

Mes derniers souvenirs avec C++ sont moins favorables :
- documentation éclatée
- quand on reprend un projet pour le compiler, j'ai toujours eu à chercher les dépendances qu'il me manquait car ce n'était pas noté sur le projet ou c'était incomplet

Vu que je ne touche plus au C++ depuis longtemps, je ne suis pas du tout informé des outils qui existent (à part des quelques témoignages/commentaires/news lus ici). Si on se base sur les CVE, il est possible que beaucoup de ces merveilleux outils n'ait pas été utilisé pour empêcher les fuites.
5  0 
Avatar de prisme60
Membre régulier https://www.developpez.com
Le 12/05/2025 à 14:54
Quand un code atteint des milliers de lignes de code, avec un historique de plus de 10 ans, avec le passage d'une dizaine de développeurs ayant chacun sa manière de coder, la dette technique est colossale.
  • En c++, rien ne t'oblige à utiliser les casts du C++, c'est tellement plus facile d'utiliser les casts du C.
  • On s'aperçoit aussi que les développeurs n'utilisent pas suffisamment les const/constexpr.
  • Que certaines personnes n'ont pas compris l'intérêt des std::shared_ptr/std::weak_ptr.
  • Certains écrivent encore du code avec les itérateurs begin() et end(), alors que les std::range pourraient être utilisés.
  • L'utilisation de nullptr / -1 pour indiquer que l'objet / variable primitive n'est pas valorisée, alors que le std::optional existe justement pour ces cas de figure.
  • Les namespaces permettent de bien cloisonnées les noms. Insuffisamment utilisés dans le code, on trouve parfois même des "using namespace std" partout dans le code, ce qui va à l'encontre de l'usage du namespace.
  • Personnellement, j'ai toujours eu du mal à bien utilisé le std::forward().

Le c++ est devenu une usine à gaz. Les modules ne mélangent très mal avec le code c++ existant. Les temps de compilations sont catastrophiques. La librairie std gère mal le mixe de chaine MBCS / Unicode. L'utilisation des streams C++ sont lourds. Le langage accuse l'héritage des années. La gestion de l'asynchrone avec les lambdas nécessite une réflexion complexes sans garde-fou.
Je déconseille le développement en C++ si il n'y a pas de bonne raison. Autant s'orienter sur du C#, du Go, du Rust, en fonction des besoins (IHM, Web, Embarqué) et de ou des plateformes visées (Certains langages ont été pensés portables, et permettent d'utiliser une librairie, avec une simple inclusion).

Pour en revenir au sujet de l'article, il est évident que si il suffisait de juste faire attention à la gestion de la mémoire pour qu'il n'y ai pas de bug, il n'y aurait aucune CVE sur les fuites mémoires, les use-after-free, les double-free. Rust avec son borrow checker fait des merveilles, et permet des optimisations qui seraient déclarées comme dangereuses sans ce filet de sécurité! Le meilleur exemple concerne les sous-chaine de caractères. En c++, on a le std::string_view, et en Rust on a les slice. Si on détruit la source de la std::string_view, on se retrouve avec un use-after-free. En revanche, Rust lui ne compile pas dans ce cas là.
4  0 
Avatar de Rep.Movs
Membre actif https://www.developpez.com
Le 12/05/2025 à 13:50
Je trouve que les réactions épidermiques sont dommageables.

Rust ou C++, ou Java, ou Python sont liés à un besoin (bien sûr, pour python c'est une plaisanterie). Chaque langage a ses spécificités et certains avantages.
Mais à chaque fois que j'ai programmé dans un langage ou un autre, j'ai pu profiter des atouts du langages - jusqu'à me trouver face à une limite de ce langage, que ce soit une limite du langage même ou une difficulté dans la maintenance.

Concernant les problèmes de mémoire, si je déteste en général faire du web, je trouve que cela amène une vraie question dans la conception et l'écriture (présente aussi sous Android): l'unité de travail.

Si on se recentre sur les unités de travail et la durée de vie de nos objets (au sens conceptuel, pas d'implémentation), en général on comprend mieux comment gérer la mémoire.

Et si on a l'avantage d'être dans un environnement qui force à cette prise en compte, voire qui carrément isole chaque unité de travail, on est dans un environnement plus sûr pour développer.

A ce sujet, on peut se rappeler du fork() qui était une implémentation du unit of work.

Dans plusieurs développements, j'ai préférer sacrifier un peu les perfs en me basant sur des exécutables "jetables" pour augmenter la résilience - et ça marche très bien, même avec des langages comme VB6
3  0