web-dev-qa-db-fra.com

Pourquoi la définition de la fonction globale en ligne dans 2 fichiers cpp différents provoque-t-elle un résultat magique?

Supposons que j'ai deux fichiers .cpp file1.cpp Et 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 avant la f1() et f2():

void f1();
void f2();

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

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

f1
f1

Whoa: Le compilateur ne choisit en quelque sorte que la définition de file1.cpp Et l'utilise également dans f2(). Quelle est l'explication exacte de ce comportement?.

Notez que la modification de inline en static est une solution à ce problème. Placer la définition en ligne dans un espace de noms sans nom résout également le problème et le programme s'imprime:

f1
f2
30
Narek Atayan

Il s'agit d'un comportement indéfini, car les deux définitions de la même fonction en ligne avec liaison externe rompent l'exigence C++ pour les objets qui peuvent être définis à plusieurs endroits, connus sous le nom de One Definition Rule:

3.2 Une règle de définition

...

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

6.1 chaque définition de D doit consister en la même séquence de jetons; [...]

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

41
dasblinkenlight

Le compilateur peut supposer que toutes les définitions de la même fonction inline sont identiques dans toutes les unités de traduction car la norme le dit. Il peut donc choisir la définition qu'il souhaite. Dans votre cas, c'était justement celui avec f1.

Notez que vous ne pouvez pas compter sur le compilateur choisissant toujours la même définition, violer la règle susmentionnée rend le programme mal formé. Le compilateur pourrait également diagnostiquer cela et éliminer les erreurs.

Si la fonction est static ou dans un espace de noms anonyme, vous avez deux fonctions distinctes appelées foo et le compilateur doit choisir celle du bon fichier.


Standardese pertinent pour référence:

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

7.1.2/4 dans N4141, mettez l'accent sur le mien.

31
Baum mit Augen

Comme d'autres l'ont noté, les compilateurs sont conformes à la norme C++ car la règle d'une définition indique que vous n'aurez qu'une seule définition d'une fonction, sauf si la fonction est en ligne, les définitions doivent être les même.

En pratique, ce qui se passe est que la fonction est signalée comme étant en ligne, et au stade de la liaison si elle s'exécute dans plusieurs définitions d'un jeton marqué en ligne, l'éditeur de liens rejette silencieusement tout sauf un. S'il s'exécute dans plusieurs définitions d'un jeton non signalé en ligne, il génère à la place une erreur.

Cette propriété est appelée inline car, avant LTO (optimisation du temps de liaison), prendre le corps d'une fonction et le "mettre en ligne" sur le site d'appel exigeait que le compilateur ait le corps de la fonction. Les fonctions inline pourraient être placées dans des fichiers d'en-tête, et chaque fichier cpp pourrait voir le corps et "incorporer" le code dans le site d'appel.

Cela ne signifie pas que le code va réellement être aligné; 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 d'éliminer les doublons. Cela inclut les compilateurs qui vérifient par ailleurs que les définitions des corps de fonction sont identiques, comme le pliage COMDAT de MSVC. Cela me rend triste, car il s'agit d'un véritable ensemble subtil de bugs.

La bonne façon de contourner votre problème est de placer la fonction dans un espace de noms anonyme. En général, vous devriez envisager de mettre tout dans un fichier source dans un espace de noms anonyme.

Un autre exemple vraiment méchant 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() différentes, toutes deux en ligne, et elles diffèrent.

Les tailles des deux classes diffèrent. Dans un cas, nous initialisons deux sizeof(double) avec 0 (Car le flottant zéro est zéro octet dans la plupart des situations).

Dans un autre, nous initialisons d'abord trois sizeof(void*) avec zéro, puis appelons .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 ignorée et utilisée par l'autre. De plus, lequel est rejeté est susceptible d'être assez déterministe dans une version complète. Dans une version partielle, cela pourrait changer l'ordre.

Alors maintenant, vous avez du code qui peut être construit et fonctionner "correctement" dans une version complète, mais une version partielle provoque une corruption de la mémoire. Et changer l'ordre des fichiers dans les makefiles pourrait endommager la mémoire, ou même changer l'ordre des fichiers lib sont liés, ou mettre à niveau votre compilateur, etc.

Si les deux fichiers cpp avaient un bloc namespace {} Contenant tout sauf les éléments que vous exportez (qui peuvent utiliser des noms d'espace de noms complets), cela ne pourrait pas se produire.

J'ai attrapé exactement ce bug en production plusieurs fois. Compte tenu de sa subtilité, je ne sais pas combien de fois il s'est glissé, attendant son moment de bondir.