Rust vs C/C++
Comparer les approches de sécurité mémoire
Même performance, sécurité différente
Rust et C++ sont tous deux des langages compilés en code machine natif, sans garbage collector, avec un accès direct au matériel. Ils visent le même créneau : systèmes d'exploitation, navigateurs, moteurs de jeux, systèmes embarqués.
Positionnement des langages :
Performance ←──────────────────────────→ Sécurité
maximale maximale
┌─────┐ ┌─────┐ ┌──────┐ ┌──────┐
│ C │ │ C++ │ │ Java │ │Python│
└─────┘ └─────┘ └──────┘ └──────┘
│ │ │ │
│ pas de GC GC + JIT interprété
│ dangereux plus lent très lent
│
│ ┌──────┐
└─────────│ Rust │ ← performance de C, sécurité de Java
└──────┘La grande différence ? C++ fait confiance au développeur pour gérer la mémoire correctement. Rust le vérifie automatiquement à la compilation.
Mémoire manuelle en C++
C++ offre plusieurs mécanismes pour gérer la mémoire, du plus dangereux au plus sûr. Le problème : aucun n'est garanti par le compilateur.
// 1. new/delete brut — dangereux
int* ptr = new int(42);
// ... et si on oublie delete ?
delete ptr; // fuite mémoire si oublié
ptr = nullptr; // bonne pratique, mais pas obligatoire
// 2. RAII — mieux, mais pas parfait
class Buffer {
int* data;
public:
Buffer(int n) : data(new int[n]) {}
~Buffer() { delete[] data; }
// Oops : pas de copy constructor → double-free !
};
// 3. Smart pointers (C++11) — le mieux
auto p = std::make_unique<int>(42);
// Libéré automatiquement
// Mais rien n'empêche de mélanger avec des raw pointersMême avec les smart pointers modernes de C++, les erreurs restent possibles :
#include <memory>
#include <vector>
void exemple_dangereux() {
std::vector<int> v = {1, 2, 3};
int& ref = v[0]; // référence vers le premier élément
v.push_back(4); // réallocation possible !
// ref est maintenant un dangling reference
// Comportement indéfini — le compilateur ne dit RIEN
std::cout << ref << std::endl;
}Le compilateur C++ ne détecte pas ce bug. Les outils comme AddressSanitizer ou Valgrind peuvent le trouver à l'exécution, mais seulement si le chemin de code est testé.
Le modèle d'ownership de Rust
En Rust, le même bug est détecté à la compilation. Le borrow checker impose des règles strictes sur les références :
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // emprunt immutable
// v.push(4); // ERREUR de compilation !
// cannot borrow `v` as mutable because it is also
// borrowed as immutable
println!("{}", first); // utilisation de l'emprunt
// Après la dernière utilisation de first,
// on peut à nouveau muter v :
v.push(4); // OK maintenant
}Comparons le même scénario côte à côte :
C++ : Rust :
┌──────────────────────┐ ┌──────────────────────┐
│ vector<int> v; │ │ let mut v = vec![]; │
│ int& ref = v[0]; │ │ let r = &v[0]; │
│ v.push_back(4); │ │ v.push(4); │
│ cout << ref; │ │ println!("{}", r); │
├──────────────────────┤ ├──────────────────────┤
│ Compile ✓ │ │ ERREUR compilation ✗ │
│ Bug silencieux │ │ Bug détecté │
│ Trouvé par ASAN │ │ Aucun outil requis │
│ (si chemin testé) │ │ Garanti à 100 % │
└──────────────────────┘ └──────────────────────┘Le borrow checker ne fonctionne pas par heuristique : il prouve mathématiquement que votre programme est sûr, ou il refuse de le compiler. C'est une garantie absolue, pas une approximation.
Concurrence sans data races
Les data races sont parmi les bugs les plus difficiles à détecter et reproduire. Ils surviennent quand deux threads accèdent à la même donnée sans synchronisation. En C++, le compilateur ne vous protège pas :
#include <thread>
#include <vector>
int main() {
std::vector<int> data;
// Deux threads accèdent au même vecteur
// sans synchronisation → DATA RACE !
std::thread t1([&data]() {
for (int i = 0; i < 1000; i++)
data.push_back(i);
});
std::thread t2([&data]() {
for (int i = 0; i < 1000; i++)
data.push_back(i);
});
t1.join(); t2.join();
// Résultat : corruption mémoire aléatoire
}En Rust, les traits Send et Sync combinés au borrow checker empêchent les data races à la compilation :
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc = compteur de références atomique (thread-safe)
// Mutex = accès exclusif garanti
let data = Arc::new(Mutex::new(Vec::new()));
let handles: Vec<_> = (0..2).map(|_| {
let data = Arc::clone(&data);
thread::spawn(move || {
for i in 0..1000 {
data.lock().unwrap().push(i);
} // le verrou est libéré automatiquement
})
}).collect();
for h in handles { h.join().unwrap(); }
// Garanti sans data race par le compilateur
}Traits de concurrence en Rust : Send → le type peut être TRANSFÉRÉ entre threads Sync → le type peut être PARTAGÉ entre threads (via &T) ┌─────────────┬──────┬──────┐ │ Type │ Send │ Sync │ ├─────────────┼──────┼──────┤ │ i32, String │ ✓ │ ✓ │ types simples │ Arc<T> │ ✓ │ ✓ │ partage thread-safe │ Mutex<T> │ ✓ │ ✓ │ accès exclusif │ Rc<T> │ ✗ │ ✗ │ un seul thread ! │ Cell<T> │ ✓ │ ✗ │ pas de partage └─────────────┴──────┴──────┘ Si un type n'est pas Send, le compilateur REFUSE de le passer à un autre thread. C'est automatique.
Gestion des erreurs
C++ utilise les exceptions pour gérer les erreurs. Le problème : rien ne force le développeur à les attraper, et le coût en performance est imprévisible.
#include <fstream>
#include <string>
std::string read_file(const std::string& path) {
std::ifstream file(path);
if (!file.is_open())
throw std::runtime_error("Cannot open file");
// L'appelant doit-il attraper l'exception ?
// Le compilateur ne l'exige pas !
std::string content;
// ... lire le fichier
return content;
}
int main() {
// Oubli du try/catch → crash à l'exécution
auto content = read_file("missing.txt");
}En Rust, les erreurs sont des valeurs retournées par les fonctions. Le type Result<T, E> force le développeur à gérer explicitement chaque erreur possible :
use std::fs;
use std::io;
fn read_file(path: &str) -> Result<String, io::Error> {
fs::read_to_string(path) // retourne Result
}
fn main() {
match read_file("missing.txt") {
Ok(content) => println!("{}", content),
Err(e) => eprintln!("Erreur : {}", e),
}
// Ou plus concis avec l'opérateur ?
// qui propage l'erreur automatiquement
}C++ exceptions vs Rust Result : C++ : Rust : ┌─────────────────────────┐ ┌─────────────────────────┐ │ throw exception │ │ return Err(e) │ │ Coût runtime (déroulage)│ │ Coût zéro (juste un │ │ Peut être ignorée │ │ enum sur la pile) │ │ Pas visible dans le type│ │ DOIT être géré │ │ Quand ? Aucune garantie │ │ Visible dans le type : │ │ │ │ fn() -> Result<T, E> │ └─────────────────────────┘ └─────────────────────────┘ Result<T, E> : ┌─────────┐ │ Ok(T) │ → contient la valeur de succès ├─────────┤ │ Err(E) │ → contient l'erreur └─────────┘ Le compilateur avertit si un Result n'est pas utilisé.
Résumé
Rust et C++ ciblent le même domaine, mais avec des philosophies opposées :
| Aspect | C++ | Rust |
|---|---|---|
| Sécurité mémoire | Convention + outils | Garantie par le compilateur |
| Concurrence | Responsabilité du dev | Send/Sync vérifiés |
| Gestion des erreurs | Exceptions (optionnelles) | Result<T,E> (obligatoire) |
| Null | nullptr (crash) | Option<T> (sûr) |
| Héritage | Classes, héritage multiple | Traits, composition |
Quand choisir Rust plutôt que C++ ? Quand la sécurité mémoire est critique (navigateurs, systèmes embarqués, serveurs), quand vous démarrez un nouveau projet sans dette technique C++, ou quand la concurrence est au coeur de votre architecture.
C++ reste pertinent pour les projets existants avec des millions de lignes de code, les moteurs de jeux matures, et les domaines où l'écosystème C++ est irremplaçable. Mais pour les nouveaux projets systèmes, Rust offre un avantage décisif : la sécurité sans compromis sur la performance.