Retour à l'accueil

Mécanisme de recherche de noms en C++ : ADL et recherche en deux phases pour les modèles

L'article explique le mécanisme complexe de recherche de noms (name lookup) en C++. Les termes clés, l'ADL et la recherche en deux phases dans les modèles sont couverts. Exemples de code et différences d'implémentation entre les compilateurs GCC et Clang sont fournis. Recommandations pour éviter les erreurs courantes.

Pourquoi les compilateurs C++ se trompent-ils avec les noms ? Nous décomposons l'ADL et la recherche en deux phases
Advertisement 728x90

# 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 :

Google AdInline article slot
// 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 :

Google AdInline article slot
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).

Google AdInline article slot

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 typename pour 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

Advertisement 728x90

Lire ensuite