Modèles C++ : pourquoi votre code ne se lie pas (et comment y remédier)
\nAvez-vous déjà écrit une élégante fonction template, soigneusement placé la déclaration dans un .h, la définition dans un .cpp, compilé le projet... et obtenu une erreur de liaison inquiétante ? undefined reference — un bonjour de 1998. Cela vous dit quelque chose ? Si oui, vous êtes au bon endroit. Si non, vous êtes soit un génie, soit vous n'avez jamais essayé de séparer les modèles. Bienvenue au club.
Dans cet article, nous allons décortiquer une question très populaire sur Stack Overflow (oui, celle avec des milliers de vues) : « Storing C++ template function definitions in a .CPP file ». Nous ne nous contenterons pas de copier la réponse — nous comprendrons pourquoi cela fonctionne ainsi, et nous apprendrons à l'appliquer en pratique. C'est parti !
\n\nLe problème : les modèles ne sont pas des fonctions ordinaires
\nImaginez que vous écrivez une bibliothèque en C++ (ou simplement un projet éducatif pour PythonLib). Vous avez une fonction template max :
// max.h\ntemplate <typename T>\nT max(T a, T b);\n// max.cpp\ntemplate <typename T>\nT max(T a, T b) {\n return (a > b) ? a : b;\n}\n// main.cpp\n#include "max.h"\nint main() {\n int result = max(3, 7); // Erreur de l'éditeur de liens !\n return 0;\n}\nLe compilateur de main.cpp ne voit que la déclaration dans le .h. Il génère un appel à la fonction max<int>. Mais où est son corps ? Dans max.cpp — mais le compilateur ne sait pas que int sera nécessaire ! Il compile max.cpp séparément et, ne voyant aucun appel à max<int>, ne crée tout simplement pas cette spécialisation. Résultat : une erreur à l'étape de liaison.
L'idée clé : un modèle n'est pas du code, mais un plan de code. Tant que le compilateur ne voit pas un type concret, il ne génère pas de code machine. Par conséquent, la séparation classique en .h et .cpp pour les modèles ne fonctionne pas par défaut.
Solution #1 : Tout dans le fichier d'en-tête (Header-Only)
\nL'approche la plus simple et la plus couramment utilisée consiste à placer à la fois la déclaration et la définition dans le fichier .h. Oui, cela augmente le temps de compilation (chaque .cpp qui inclut cet en-tête va réinstancier le modèle), mais en contrepartie, aucun problème de liaison.
// max.h (header-only)\n#ifndef MAX_H\n#define MAX_H\n\ntemplate <typename T>\nT max(T a, T b) {\n return (a > b) ? a : b;\n}\n\n#endif\nAvantages : simplicité, aucune erreur de liaison, idéal pour les petits projets ou les bibliothèques où les modèles sont rarement utilisés.
\nInconvénients : gonflement du code à la compilation, augmentation du temps de construction, impossibilité de masquer l'implémentation (si vous distribuez une bibliothèque sous forme de binaire compilé).
\n\nSolution #2 : Instanciation explicite (Explicit Instantiation)
\nEt voici le héros de notre article. Si vous savez à l'avance avec quels types le modèle sera utilisé, vous pouvez demander explicitement au compilateur de créer ces spécialisations dans le fichier .cpp. C'est la réponse à la question « Storing C++ template function definitions in a .CPP file ».
// max.h\n#ifndef MAX_H\n#define MAX_H\n\ntemplate <typename T>\nT max(T a, T b);\n\n// Informons l'éditeur de liens que les spécialisations seront ailleurs\nextern template int max(int, int);\nextern template double max(double, double);\n\n#endif\n// max.cpp\n#include "max.h"\n\ntemplate <typename T>\nT max(T a, T b) {\n return (a > b) ? a : b;\n}\n\n// Instancions explicitement les types nécessaires\ntemplate int max(int, int);\ntemplate double max(double, double);\n// main.cpp\n#include "max.h"\nint main() {\n int a = max(3, 7); // OK\n double b = max(3.14, 2.71); // OK\n // float c = max(1.0f, 2.0f); // Erreur