Bref sur ODR – la One Definition Rule

Pour en savoir plus, lisez ce texte sur la mécanique de compilation en C++. Vous apprécierez peut-être aussi ../AuSecours/specifications-entreposage.html qui discute de questions connexes

C++, comme C, préconise la compilation séparée. En ce sens, chaque fichier source (typiquement, un fichier portant l'extension .cpp) est compilé séparément des autres, puis les fichiers objets résultants (.o, .obj, .a, .lib, etc.) sont assemblés en un binaire (.exe, .dll, .so, etc.) par un mécanisme nommé l'édition des liens.

C++ exige qu'il n'y ait qu'une seule définition pour chaque chose, tout en tolérant qu'il y ait plusieurs déclarations pour une même entité. Pour cette raison, un fichier source est en violation d'ODR s'il a plus d'une définition pour un même nom, et ce même si les deux définitions sont identiques. Notez qu'une violation d'ODR est techniquement un cas de comportement indéfini, alors mieux vaut les éviter!

Par exemple, ci-dessous, le programme vilain.cpp est en violation d'ODR à cause de la double définition de la variable x et de la fonction f(). Notez que y ne pose pas de problème car les deux variables de ce nom ne sont pas définies au même niveau hiérarchique, ce qui évite les conflits de noms. Notez aussi que g() ne pose pas de problème à la compilation (l'appel respecte la signature annoncée par son prototype), mais il faudra qu'une définition (unique) soit rendue disponible au moment de l'édition des liens pour que cette étape de génération de code soit un succès :

vilain.cpp
int x = 3,
    y = 4;
int f(int n) {
   return n + x;
}
int g(int);
#include <iostream>
int main() {
   using namespace std;
   int y = f(-2);
   cout << g(y) << endl;
}
float x = 3.14159f; // <-- violation d'ODR
int f(int n) { // <-- violation d'ODR (que le corps de la fonction soit le même qu'avant ou pas)
   return n * n;
}

Sachant de l'inclusion des fichiers d'en-têtes est lexicale, il faut comprendre ce qui peut être placé (ou non) dans un tel fichier pour éviter les erreurs. Les règles sont simples, au fond : un .h ne peut contenir que des déclarations ou des définitions inline, donc des éléments qui ne risquent pas de causer de violations d'ODR :

  • Des déclarations a priori (Forward Class Declarations), qui déclarent (annoncent) des types sans en donner le détail dans l'immédiat
    • Dans les exemples à droite, class Surprise; et friend class Quatre; (cette dernière dans la portée de Trois) sont des exemples de telles déclarations
  • Des définitions de types (struct, class, enum, union, etc.) et des alias (typedef, using)
    • Dans les exemples à droite, notez les types Point, Trois et Tableau<N,T>
  • Des prototypes (signatures) de fonctions globales, car ce ne sont que des déclarations, pas des définitions
    • Dans les exemples à droite, on note la déclaration de la fonction cadeau() et celle de la fonction carre(int), mais pas la définition de cette dernière, en plus de la déclaration de la fonction globale operator==(const Point&,const Point&)
  • Des déclarations de variables (extern), sans définitions (il faut alors que la définition soit unique et placée dans un .cpp qui sera lié au moment de l'édition des liens)
    • Dans les exemples à droite, PI est une telle déclaration
  • Des définitions de variables inline (depuis C++ 17)
    • Dans les exemples à droite, Trois::VALTXT en est un exemple
  • Des définitions de variables constexpr
    • Dans les exemples à droite, Trois::VAL en est un exemple
  • Des définitions de variables globales qualifiées static, ou encore placées dans un namespace anonyme (ces définitions n'apparaissent pas à l'édition des liens)
  • Des définitions de fonctions globales inline
    • Dans les exemples à droite, la définition de la fonction carre(int) est un cas d'une telle fonction
  • Des définitions de méthodes à même la déclaration de leur classe. Pour le compilateur, ceci signifie que la méthode doit être considérée inline

Note importante : tout template doit être défini à même le .h qui le déclare, car le code effectif du template est généré par le compilateur au point d'utilisation et doit, pour cette raison, être visible au compilateur à ce moment.

#ifndef A_H
#define A_H
//
// Ce qui suit est légal dans un .h
//
struct Point {
   int x{}, y{}, z{};
   Point() = default;
   constexpr Point(int x, int y, int z) : x{x}, y{y}, z{z} {
   }
   friend bool operator==(const Point &, const Point&);
   // ...
};
class Surprise; // déclaration a priori
Surprise cadeau(); // Ok, déclaration de fonction
#include <string>
class Trois {
   static constexpr auto VAL = 3;
   static inline const string VALTXT = "Trois";
public:
   bool est_impair() const {
      return true;
   }
   static constexpr int valeur() {
      return VAL;
   }
   bool est_premier() const;
   friend class Quatre;
};
long carre(int); // Ok, déclaration
//
// PI doit être définie dans un .cpp accessible à l'édition des liens
//
extern const float PI;
inline long carre(int n) { // Ok, inline
   return n * n;
}
template <class T>
   bool est_pair(const T& val) { // Ok, template
      return val % 2 == 0;
   }
template <>
   bool est_pair(Trois) {
      return false;
   }
#include <cassert>
template <int N, class T>
   class Tableau {
   public:
      using value_type = T;
      using size_type = int;
   private:
      value_type elems[N]{};
   public:
      size_type size() const noexcept {
         return N;
      }
      value_type& operator[](size_type n) {
         assert(0 <= n && n < size());
         return elems[n];
      }
      value_type operator[](size_type n) const {
         assert(0 <= n && n < size());
         return elems[n];
      }
   };
//...

Il ne faut donc pas mettre dans un .h les choses suivantes :

  • Définition de fonction globale non-inline
  • Définition de méthode hors de la déclaration de sa classe
  • Définition de variable globale
  • Définition de constante ou de variable non-constexpr et non-inline, outre les constantes entières (qui ont un statut privilégié)

Dans ces cas, le fait d'inclure le même .h dans deux .cpp d'un même projet entraînera une double définition de l'entité « offensante », donc une violation d'ODR.

// ...
//
// Ce qui suit risque de provoquer une violation d'ODR
//
int init = 0;
int cube(int n) {
   return n * carre(n);
}
bool operator!=(const Point &p0, const Point &p1) {
   return ! (p0 == p1);
}
bool Trois::est_premier() const {
   return true;
}
#endif

Lectures complémentaires

Quelques liens pour enrichir le propos :


Valid XHTML 1.0 Transitional

CSS Valide !