Collision de nom de classe C ++

Pour le code C ++ suivant, je reçois un comportement inattendu. Le comportement a été vérifié avec les récents GCC, Clang et MSVC ++. Pour le déclencher, il est nécessaire de diviser le code entre plusieurs fichiers.

def.h

#pragma once template struct Base { void call() {hook(data);} virtual void hook(T& arg)=0; T data; }; 

foo.h

 #pragma once void foo(); 

foo.cc

 #include "foo.h" #include  #include "def.h" struct X : Base { virtual void hook(int& arg) {std::cout << "foo " << arg << std::endl;} }; void foo() { X x; x.data=1; x.call(); } 

bar.h

 #pragma once void bar(); 

bar.cc

 #include "bar.h" #include  #include "def.h" struct X : Base { virtual void hook(double& arg) {std::cout << "bar " << arg << std::endl;} }; void bar() { X x; x.data=1; x.call(); } 

main.cc

 #include "foo.h" #include "bar.h" int main() { foo(); bar(); return 0; } 

Production attendue:

 foo 1 bar 1 

Sortie réelle:

 bar 4.94066e-324 bar 1 

Ce à quoi je m’attendais:

A l’intérieur de foo.cc, une instance de X définie dans foo.cc est créée et l’appelant call () appelle l’implémentation de hook () dans foo.cc. Même chose pour la barre.

Qu’est-ce qui se passe réellement:

Une instance de X telle que définie dans foo.cc est créée dans foo (). Mais lorsque vous appelez un appel, il ne dissortingbue pas () défini dans foo.cc mais à hook () défini dans bar.cc. Cela conduit à la corruption, car l’argument de hook est toujours un int, pas un double.

Le problème peut être résolu en plaçant la définition de X dans foo.cc dans un autre espace de noms que la définition de X dans bar.cc

Donc finalement la question: Il n’y a pas d’avertissement du compilateur à ce sujet. Ni gcc, ni clang ou MSVC ++ n’ont même montré un avertissement à ce sujet. Ce comportement est-il valide selon la norme C ++?

La situation semble être un peu construite, mais cela s’est produit dans un scénario réel. J’écrivais des tests avec rapidcheck, où les actions possibles sur une unité à tester sont définies en tant que classes. La plupart des classes de conteneur ont des actions similaires, de sorte que lors de l’écriture de tests pour une queue et un vecteur, des classes avec des noms tels que “Clear”, “Push” ou “Pop” peuvent apparaître plusieurs fois. Comme ils ne sont nécessaires que localement, je les place directement dans les sources où les tests sont exécutés.

Le programme est illuminé, car il viole la règle à une définition en ayant deux définitions différentes pour la classe X Donc, ce n’est pas un programme C ++ valide. Notez que la norme permet spécifiquement aux compilateurs de ne pas diagnostiquer cette violation. Les compilateurs sont donc conformes, mais le programme n’est pas C ++ valide et a donc un comportement indéfini lorsqu’il est exécuté (et donc tout peut arriver).

Vous avez deux classes X nom identique, mais différentes, dans différentes unités de compilation, ce qui rend le programme mal formé, car il existe maintenant deux symboles portant le même nom. Étant donné que le problème ne peut être détecté que lors de la liaison, les compilateurs ne sont pas en mesure de le signaler.

La seule façon d’éviter ce genre de chose est de placer un code qui n’est pas destiné à être exporté (en particulier tout le code qui n’a pas été déclaré dans un fichier d’en-tête) dans un espace de noms anonyme ou non nommé :

 #include "foo.h" #include  #include "def.h" namespace { struct X : Base { virtual void hook(int& arg) {std::cout << "foo " << arg << std::endl;} }; } void foo() { X x; x.data=1; x.call(); } 

et de manière équivalente pour bar.cc En fait, c'est le but principal (unique?) Des espaces de noms sans nom.

Le simple fooX nom de vos classes (par exemple, fooX et barX ) peut fonctionner pour vous en pratique, mais n’est pas une solution stable, car il n’ya aucune garantie que ces noms de symboles ne soient pas utilisés par des bibliothèques tierces obscures chargées sur link- ou run-time (maintenant ou à un moment donné dans le futur).