Présentation
L’auteur, fort de plus de quinze ans d’expérience sur des bases C++ de 0,5 à 1 MLOC, décrit comment les index‑handles remplacent les pointeurs bruts et les smart‑pointers. Dans des projets contenant de dizaines à plusieurs centaines de milliers d’objets alloués séparément, les pointeurs provoquent de nombreux défauts de cache, de la fragmentation et des « fake leaks » – des fuites invisibles aux outils de débogage parce qu’un smart‑pointer persiste.
Architecture des handles
Le principe repose sur quatre piliers : centraliser la gestion mémoire dans des systems (rendu, physique, IA…), regrouper chaque type d’objet dans un tableau contigu, retourner aux appelants un handle composé d’un indice de tableau et de bits de contrôle, convertir cet indice en pointeur uniquement lorsqu’une accès direct est indispensable, et enfin éviter tout stockage de pointeur à long terme. Le handle utilise le nombre minimal de bits pour l’indice et consacre les bits restants à des vérifications de validité (par exemple un compteur de génération ajouté le 28‑nov‑2018 pour prévenir les collisions).
Optimisations et limites
En regroupant les objets dans des tableaux, le système élimine les allocations individuelles ; il ne fait que marquer les cases libres, ce qui ressemble à un pool allocator. Cette approche réduit le nombre d’appels à l’allocateur général, diminue la surcharge de métadonnées et garantit un empaquetage dense, améliorant ainsi la localisation spatiale et les taux de hit du cache L1/L2. Cependant, la conversion ponctuelle d’un handle en pointeur introduit un coût de calcul (extraction de l’indice, vérification du compteur). De plus, la méthode exige que toutes les créations et destructions passent par l’API du système ; dans un code orienté objet fortement découpé, cela peut nécessiter une refonte importante. Enfin, le compteur de génération, s’il déborde, doit être réinitialisé, ce qui impose une logique supplémentaire.
Implémentation pratique
Un système typique initialise un tableau fixe ou redimensionnable au démarrage, maintient une pile de slots libres et associe chaque slot à un handle structuré ainsi :
struct Handle {
uint32_t index : 20; // jusqu'à 1 048 576 slots
uint32_t gen : 12; // compteur de génération
};Lorsqu’un objet est libéré, le compteur de génération du slot est incrémenté, rendant tout handle antérieur invalide. L’accès se fait via une fonction Item* get(Handle h) qui vérifie h.gen == slot.gen avant de retourner &array[h.index]. Cette vérification élimine les dereferences de pointeurs dangling et rend les erreurs détectables immédiatement.