Réponse à la question.
Historique de la question
Avant C++23, la mise en œuvre du polymorphisme statique nécessitait le modèle de template curieusement récurrent (CRTP). Cette approche obligeait les classes dérivées à hériter d'une classe de base template instanciée avec le type dérivé lui-même. Bien que fonctionnelle, la CRTP produisait un code verbeux et des hiérarchies d'héritage complexes qui étaient difficiles à maintenir.
Le problème
Le problème central était que les fonctions membres dans les bases CRTP ne pouvaient pas déduire le type dérivé réel sans paramètres template explicites. Cette limitation obligeait les développeurs à caster this au type dérivé manuellement, créant un code fragile qui se brisait lorsque les chaînes d'héritage changeaient. De plus, la CRTP empêchait un refactoring facile et rendait les interfaces moins intuitives pour les utilisateurs non familiers avec la métaprogrammation des templates.
La solution
C++23 a introduit le paramètre d'objet explicite (dédoublant this), permettant aux fonctions membres de déclarer this comme un paramètre explicite avec un type déduit. En écrivant void func(this auto&& self), la fonction accepte n'importe quel type d'objet, permettant le polymorphisme statique par surcharge plutôt que par héritage. Cette approche élimine complètement la CRTP, produisant un code plus propre qui prend en charge le polymorphisme ouvert.
// Approche de C++23 struct Vector { float x, y; template<typename Self> auto magnitude(this Self&& self) { return std::sqrt(self.x * self.x + self.y * self.y); } }; // L'utilisation fonctionne sans héritage Vector v{3.0f, 4.0f}; float len = v.magnitude();
Situation du quotidien
Une équipe de moteur de jeu avait besoin d'une bibliothèque de vecteurs mathématiques prenant en charge à la fois les chemins de compilation CPU et GPU. La bibliothèque nécessitait des opérations génériques comme magnitude() et normalize() qui fonctionnaient avec des types de précision float, double et half tout en maintenant une abstraction sans coût.
La première approche considérée était la CRTP avec une classe de base VectorBase<Derived, T>. Cela permettait le polymorphisme à la compilation, mais introduisait une complexité significative. Chaque nouveau type de vecteur devait hériter de la base et se passer comme paramètre template, causant un code verbeux et des erreurs d'instanciation de template cryptiques lors du refactoring. La maintenance était difficile car changer l'interface de base nécessitait de mettre à jour toutes les classes dérivées.
La deuxième approche considérée était la surcharge de fonctions avec des fonctions libres et le dispatching par tag. Cela évitait l'héritage mais brisait la conception orientée objet préférée par l'équipe graphique. Il fallait passer des instances de vecteurs comme paramètres plutôt que d'appeler des méthodes, ce qui semblait unnatural pour des objets mathématiques. De plus, cela compliquait la surface de API et rendait l'enchaînement de méthodes impossible.
La solution choisie était la syntaxe de paramètre d'objet explicite de C++23. L'équipe a réécrit les classes de vecteurs pour utiliser des paramètres auto&& self, permettant le polymorphisme statique sans héritage. Cette approche préservait la syntaxe intuitive vec.magnitude() tout en prenant en charge la programmation générique et en éliminant le surcoût des templates.
Le résultat a été une réduction de 40 % des erreurs de compilation liées aux templates et une productivité accrue des développeurs. La base de code est devenue significativement plus maintenable, et l'enchaînement de méthodes fonctionnait parfaitement avec tous les types de vecteurs. L'équipe a réussi à déployer la bibliothèque à la fois pour les cibles CPU et GPU sans la complexité de CRTP.
Ce que les candidats manquent souvent
Pourquoi la déduction du paramètre d'objet explicite échoue-t-elle lorsque la fonction membre est déclarée const mais que le type déduit n'est pas qualifié const ?
Les candidats manquent souvent que lorsqu'ils utilisent this auto&& self, le type déduit inclut des qualifications cv de l'expression. Si une fonction est appelée sur un objet const, le type déduit automatiquement à const T&.
Cependant, si le candidat déclare par erreur le paramètre comme this T self (par valeur) sur un objet const, il tente de copier. Cela peut déclencher un constructeur de copie supprimé ou des opérations de copie profonde coûteuses.
L'idée clé est que auto&& suit les règles de collage des références et préserve la constance automatiquement. Cela en fait la forme préférée pour les fonctions membres génériques, garantissant la correction const sans qualification explicite.
Comment le paramètre d'objet explicite permet-il des modèles de lambda récursifs sans le surcoût de std::function ?
Les candidats oublient souvent que les paramètres d'objet explicites permettent aux lambdas de s'appeler sans surcoût de type std::function. En déclarant la lambda avec un paramètre auto explicite qui accepte elle-même, elle peut se récursivement à l'aide de ce paramètre.
Par exemple, auto factorial = [](this auto&& self, int n) -> int { return n <= 1 ? 1 : n * self(n-1); }; crée une lambda récursive sans surcoût. Le compilateur connaît le type exact à la compilation, permettant un inlining et une optimisation complets.
Sans cette fonctionnalité, la récursivité nécessite std::function, ce qui introduit un surcoût d'effacement de type et empêche l'inlining. Alternativement, les développeurs utilisaient des combinatoires à point fixe avec une syntaxe complexe qui obscurcit l'intention.
Le paramètre d'objet explicite fournit une référence directe à soi avec une préservation complète du type. Ce modèle maintient les performances tout en prenant en charge des algorithmes récursifs élégants dans le code générique.
Pourquoi utiliser des paramètres d'objet explicites empêche-t-il la formation de hiérarchies de classes traditionnelles tout en permettant un comportement polymorphe ?
Ce point subtil confond de nombreux candidats. Le polymorphisme traditionnel repose sur l'héritage et les fonctions virtuelles, créant un couplage étroit entre les classes de base et dérivées via les vtables.
Les paramètres d'objet explicites permettent un "polymorphisme ouvert" où tout type fournissant l'interface requise peut utiliser la fonction. Il n'est pas nécessaire d'hériter d'une classe de base commune ou d'avoir des destructeurs virtuels.
La distinction clé est qu'avec des paramètres d'objet explicites, le polymorphisme se résout au moment de la compilation par le biais de la résolution de surcharge. Il n'y a pas de type de classe de base à caster, ce qui empêche le découpage d'objet et élimine le surcoût de vtable.
Cependant, cela signifie également que vous ne pouvez pas stocker des objets hétérogènes dans un conteneur de pointeurs de classe de base sans effacement de type. Le polymorphisme est strictement statique, offrant des avantages en termes de performances mais des contraintes architecturales différentes de celles du polymorphisme dynamique.