30 votes

Pourquoi la définition d'une fonction globale en ligne dans deux fichiers cpp différents donne-t-elle un résultat magique ?

Supposons que j'ai deux fichiers .cpp file1.cpp y file2.cpp :

// file1.cpp
#include <iostream>

inline void foo()
{
    std::cout << "f1\n";
}

void f1()
{
    foo();
}

et

// file2.cpp
#include <iostream>

inline void foo()
{
   std::cout << "f2\n";
}

void f2()
{
    foo();
}

Et dans main.cpp J'ai déclaré en avance le f1() y f2() :

void f1();
void f2();

int main()
{
    f1();
    f2();
}

Résultat ( ne dépend pas du build, même résultat pour les builds debug/release ) :

f1
f1

Whoa : Compilateur en quelque sorte prend seulement la définition de file1.cpp et l'utilise également dans f2() . Quelle est l'explication exacte de ce comportement ?

Notez que le fait de changer inline a static est une solution à ce problème. Le fait de placer la définition inline dans un espace de nom non nommé résout également le problème et le programme s'imprime :

f1
f2

40voto

dasblinkenlight Points 264350

Il s'agit d'un comportement non défini, car les deux définitions de la même fonction en ligne avec lien externe enfreignent l'exigence C++ relative aux objets pouvant être définis à plusieurs endroits, connue sous le nom de Règle de la définition unique :

3.2 Règle de la définition unique

...

  1. Il peut y avoir plus d'une définition d'un type de classe (article 9), d'un type d'énumération (7.2), d'une fonction en ligne avec lien externe (7.1.2), d'un modèle de classe (article 14), [...] dans un programme, à condition que chaque définition apparaisse dans une unité de traduction différente, et que les définitions satisfassent aux exigences suivantes. Étant donné une telle entité nommée D définie dans plus d'une unité de traduction, alors

6.1 chaque définition de D doit être constituée de la même séquence de jetons ; [...]

Ce n'est pas un problème avec static car la règle de définition unique ne s'applique pas à elles : C++ considère static les fonctions définies dans différentes unités de traduction soient indépendantes les unes des autres.

30voto

Baum mit Augen Points 3571

Le compilateur peut supposer que toutes les définitions d'une même inline sont identiques dans toutes les unités de traduction parce que la norme le dit. Elle peut donc choisir la définition qu'elle veut. Dans votre cas, il s'agit de celle qui contient f1 .

Notez que vous ne pouvez pas compter sur le fait que le compilateur choisisse toujours la même définition, la violation de la règle susmentionnée rend le programme mal formé. Le compilateur pourrait également diagnostiquer cela et se tromper.

Si la fonction est static ou dans un espace de nom anonyme, vous avez deux fonctions distinctes appelées foo et le compilateur doit choisir celui qui se trouve dans le bon fichier.


Normes pertinentes pour référence :

Une fonction en ligne doit être définie dans chaque unité de traduction dans laquelle elle est utilisée. et aura exactement la même définition dans tous les cas (3.2) . [...]

7.1.2/4 dans N4141, je souligne le mien.

11voto

Yakk Points 31636

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.

-3voto

PMar Points 9

POINT DE CLARIFICATION :

Bien que la réponse enracinée dans la règle inline C++ soit correcte, elle ne s'applique que si les deux sources sont compilées ensemble. Si elles sont compilées séparément, alors, comme l'a noté un commentateur, chaque fichier objet résultant contiendrait son propre "foo()". CEPENDANT : Si ces deux fichiers objets sont ensuite liés ensemble, comme les deux 'foo()' sont non-statiques, le nom 'foo()' apparaît dans la table des symboles exportée des deux fichiers objets ; alors l'éditeur de liens doit fusionner les deux entrées de la table, donc tous les appels internes sont liés à l'une des deux routines (vraisemblablement celle du premier fichier objet traité, puisqu'elle est déjà liée [c'est-à-dire que l'éditeur de liens traiterait le second enregistrement comme 'extern' indépendamment de la liaison]).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X