# Comment le compilateur C++ démêle l'écheveau des noms : ADL, modèles et recherche en deux phases
La recherche de noms est l'une des étapes les plus complexes de la compilation C++. En raison de l'évolution historique du langage, les règles de recherche sont devenues labyrinthiques, entraînant des erreurs inattendues même chez les développeurs expérimentés. Décomposons le fonctionnement du mécanisme de recherche et les raisons pour lesquelles les compilateurs se comportent différemment.
Termes clés : Comment le compilateur classe les noms
Le compilateur utilise une terminologie précise pour analyser les noms dans le code. Comprendre ces termes est essentiel pour percer le « magie » de la recherche.
Identificateur non qualifié (unqualified-id) — la forme basique d'un nom, par exemple speed, foo ou my_var. Cela inclut aussi les opérateurs, destructeurs et littéraux définis par l'utilisateur. Exemples :
// Nekvalifitsirovannye names
vector<int> v; // 'vector' — unqualified name
sort(v.begin(), v.end()); // 'sort' — unqualified name
Nom qualifié (qualified-id) — un nom avec l'opérateur de résolution de portée ::, par exemple std::vector ou Foo::bar. Ici Foo agit comme qualificateur, et bar comme nom terminal.
// Kvalifitsirovannye names
std::vector<int> v; // 'vector' — qualified name
Foo::bar(); // 'bar' — terminal qualified name
Template-id — un nom de modèle avec des arguments explicites, par exemple std::vector<int>.
// Template-id
std::vector<int> v1; // 'vector<int>' — template-id
std::pair<int, float> p; // 'pair<int, float>' — template-id
Nom terminal — le nom final dans la chaîne recherché par le compilateur. Par exemple, dans obj->f() le nom terminal est f, dans ns::Foo::bar() c'est bar. C'est crucial dans les modèles :
template<typename T>
void wrapper(T& obj) {
obj.size(); // 'size' — terminal name
T::value_type x; // 'value_type' — terminal qualified name
}
ADL : Quand les arguments mènent la danse
La recherche dépendante des arguments (ADL) est un mécanisme qui étend automatiquement la portée de recherche des fonctions en fonction des types d'arguments. Sans ADL, l'utilisation d'opérateurs surchargés pour des types définis par l'utilisateur serait malaisée.
Considérez cet exemple :
namespace sak {
struct BigNum { int value; };
std::ostream& operator<<(std::ostream& os, const BigNum& n) {
return os << "BigNum(" << n.value << ")";
}
}
int main() {
sak::BigNum n(42);
std::cout << n; // ADL nakhodit operator<< in sak
}
Le compilateur voit que n est de type sak::BigNum et ajoute l'espace de noms sak à la portée de recherche pour operator<<. Sans ADL, il faudrait écrire sak::operator<<(std::cout, n).
L'ADL a été proposée par Andrew Koenig en 1995 (document N0645), bien que l'idée soit utilisée chez AT&T Bell Labs. La norme l'a codifiée dans [basic.lookup.koenig]. Cependant, l'ADL a ses pièges : une dépendance excessive peut causer des conflits si deux bibliothèques définissent des fonctions de même nom dans des espaces de noms différents.
Algorithme ADL :
- La recherche non qualifiée ordinaire est effectuée.
- Pour les appels de fonctions, l'ADL entre en jeu : les espaces de noms des types d'arguments sont ajoutés à la recherche.
- Si le nom est qualifié (p. ex.,
std::sort), l'ADL n'est pas appliquée.
Recherche en deux phases dans les modèles : Pourquoi GCC et Clang divergent
Dans les modèles, la recherche de noms est divisée en deux phases :
- Phase 1 (définition du modèle) : recherche des noms non dépendants (indépendants des paramètres du modèle).
- Phase 2 (instanciation) : recherche des noms dépendants (dépendants des paramètres du modèle), où l'ADL s'applique.
Historiquement, les compilateurs ont interprété ces règles différemment. GCC avant la version 7 utilisait un « parsing paresseux » : le corps du modèle n'était pas analysé au moment de la définition, et tous les noms étaient recherchés lors de l'instanciation. Cela entraînait une non-conformité à la norme.
Exemple de divergence :
void foo(int) {} // globalnaya foo
template<typename T>
void bar(T x) {
foo(x); // zavisimoe name — ischetsya in phase 2 (ADL), vsyo correct
foo(42); // NEzavisimoe name — must iskatsya in phase 1
}
namespace myns {
struct MyType {};
void foo(MyType) {}
}
bar(myns::MyType{}); // GCC: kompiliruetsya, Clang: oshibka
Dans Clang, foo(42) est recherché en phase 1 et ne trouve pas de surcharge adaptée (le foo(int) global ne correspond pas pour MyType), causant une erreur. GCC retarde la recherche jusqu'à l'instanciation et trouve foo(int).
Le comité de normalisation a interdit la recherche dans les classes de base dépendantes pendant la phase 1. Considérez cet exemple :
template<typename T>
struct Base {};
template<>
struct Base<int> {
void process() {}
};
template<typename T>
struct Derived : Base<T> {
void run() {
process(); // Error: poszukiwanie in zavisimoy baze zapreschyon in phase 1
}
};
Si la recherche dans Base<T> était autorisée en phase 1, le code compilerait pour T=int mais pas pour T=float. Cela briserait la prévisibilité des modèles.
Ce qui compte : Points clés
Lors du travail avec la recherche de noms en C++, gardez ces points à l'esprit :
- Le nom terminal est le point de mire du compilateur. Identifiez-le pour savoir où chercher.
- L'ADL étend la recherche aux espaces de noms des arguments. Idéal pour les opérateurs, mais peut causer des conflits.
- Dans les modèles, les noms non dépendants sont recherchés à la définition, les dépendants à l'instanciation. Utilisez
typenamepour les types dépendants. - Les compilateurs gèrent différemment les cas limites. Testez avec GCC, Clang et MSVC.
- Pour les classes de base dépendantes, qualifiez explicitement les noms avec
this->ou des portées.
— Editorial Team
Aucun commentaire pour le moment.