Comme d'autres l'ont fait remarquer, les compilateurs sont conformes à la norme C++ parce que l'option Une règle de définition stipule que vous ne devez avoir qu'une seule définition d'une fonction, sauf si la fonction est inline, les définitions doivent être identiques.
En pratique, ce qui se passe, c'est que la fonction est marquée comme inline, et à l'étape de la liaison, si elle rencontre de multiples définitions d'un token marqué inline, l'éditeur de liens rejette silencieusement toutes les définitions sauf une. S'il rencontre plusieurs définitions d'un jeton non marqué inline, il génère une erreur.
Cette propriété est appelée inline
car, avant le LTO (link time optimization), le fait de prendre le corps d'une fonction et de l'"inliner" à l'emplacement de l'appel exigeait que le compilateur dispose du corps de la fonction. inline
Les fonctions pourraient être placées dans des fichiers d'en-tête, et chaque fichier cpp pourrait voir le corps et "intégrer" le code dans le site d'appel.
Cela ne signifie pas que le code est en fait Il permet plutôt aux compilateurs de l'intégrer plus facilement.
Cependant, je ne connais pas de compilateur qui vérifie que les définitions sont identiques avant de rejeter les doublons. Cela inclut les compilateurs qui vérifient autrement que les définitions des corps de fonctions sont identiques, comme le pliage COMDAT de MSVC. Cela me rend triste, car il s'agit d'un ensemble de bogues très subtils.
La bonne façon de contourner votre problème est de placer la fonction dans un espace de nom anonyme. En général, vous devriez envisager de mettre tout dans un fichier source dans un espace de nom anonyme.
Un autre exemple vraiment désagréable de cela :
// A.cpp
struct Helper {
std::vector<int> foo;
Helper() {
foo.reserve(100);
}
};
// B.cpp
struct Helper {
double x, y;
Helper():x(0),y(0) {}
};
Les méthodes définies dans le corps d'une classe sont implicitement en ligne . La règle ODR s'applique. Ici, nous avons deux Helper::Helper()
tous deux en ligne, et ils diffèrent.
Les tailles des deux classes diffèrent. Dans un cas, nous initialisons deux sizeof(double)
con 0
(car le zéro flottant est un zéro octet dans la plupart des situations).
Dans un autre, nous commençons par initialiser trois sizeof(void*)
avec zéro, puis appeler .reserve(100)
sur ces octets en les interprétant comme un vecteur.
Au moment de la liaison, l'une de ces deux implémentations est rejetée et utilisée par l'autre. De plus, celle qui est rejetée est susceptible d'être assez déterminante dans une construction complète. Dans une construction partielle, l'ordre pourrait changer.
Donc maintenant vous avez du code qui peut se construire et fonctionner "bien" dans une construction complète, mais une construction partielle provoque une corruption de la mémoire. Et changer l'ordre des fichiers dans les makefiles peut provoquer une corruption de la mémoire, ou même changer l'ordre dans lequel les fichiers lib sont liés, ou mettre à jour votre compilateur, etc.
Si les deux fichiers cpp ont un namespace {}
contenant tout sauf les éléments que vous exportez (qui peuvent utiliser des noms d'espaces entièrement qualifiés), cela ne peut pas se produire.
J'ai attrapé exactement ce bug en production plusieurs fois. Comme il est très subtil, je ne sais pas combien de fois il s'est glissé entre les mailles du filet, attendant le moment de bondir.