HOWTO · C++

Redéfinition du type de classe en C++

Cet article explique comment résoudre la redéfinition du type de classe en C++.

Sur cette page

Une définition de classe C++ donne à un nom sa disposition en mémoire et ses déclarations de membres. Redéfinir cette classe dans la même portée est incorrect : le compilateur signale donc une erreur de redéfinition de classe. Pour corriger le problème, repérez la définition en double, puis supprimez-la ou regroupez-la avec l’autre. L’héritage et la composition peuvent servir à ajouter des comportements à un type distinct, mais aucun des deux ne permet de définir deux fois la classe d’origine.

Comprendre l’erreur de redéfinition de classe C++

Une définition de classe introduit un type. Dans une même portée, un nom de type ne peut pas être défini deux fois comme classe, même si les deux définitions contiennent des membres identiques. Par exemple, deux définitions de class DelftStack { }; dans la même unité de traduction produisent un diagnostic tel que redefinition of 'class DelftStack'. La formulation exacte et les emplacements indiqués varient selon le compilateur.

C’est différent d’une déclaration anticipée telle que class DelftStack;, qui annonce un type sans en définir les membres. Une définition complète ultérieure est permise après cette déclaration. L’erreur concerne les définitions répétées, pas chaque mention répétée d’un nom de classe.

Repérer les définitions en double, y compris dans les fichiers inclus

Commencez par relever le nom de classe indiqué dans le diagnostic et examinez la ligne signalée ainsi que l’emplacement antérieur mentionné par le compilateur. Cherchez une autre définition dans le fichier source courant. Suivez ensuite ses directives #include : un fichier d’en-tête peut définir la même classe, ou deux fichiers inclus peuvent chacun fournir une définition portant ce nom.

Un fichier d’en-tête inclus plusieurs fois peut répéter son contenu dans une unité de traduction s’il n’utilise ni garde d’inclusion ni #pragma once. Une garde d’inclusion conventionnelle est la technique de préprocesseur portable ; #pragma once est une directive de compilateur couramment prise en charge, mais elle ne fait pas partie de la norme ISO C++. Ces deux techniques empêchent de traiter plusieurs fois le même fichier d’en-tête ; aucune ne corrige deux fichiers différents qui définissent chacun la même classe. Vérifiez aussi si un fichier source inclut un fichier d’en-tête qui définit la classe tout en recopiant lui-même le corps de cette classe.

Corriger l’erreur en ne conservant qu’une définition

Choisissez une définition de référence et supprimez la copie accidentelle. Si les deux copies contiennent des membres utiles, regroupez-les dans une seule définition, puis mettez à jour les appels si nécessaire. Si la définition appartient à un fichier d’en-tête, incluez celui-ci au lieu de recopier le corps de la classe dans les fichiers source. Ajoutez une garde d’inclusion conventionnelle, ou utilisez la directive #pragma once, couramment prise en charge, pour éviter que le même fichier d’en-tête soit traité plusieurs fois.

Ne renommez une classe que si elle représente un type réellement différent, avec un rôle distinct. Renommer une copie accidentelle peut masquer le problème de conception ou d’inclusion et laisser le programme avec deux types concurrents. Avant de changer les noms, vérifiez que le programme a bien besoin des deux types et mettez leurs utilisations à jour de façon cohérente.

Étendre une classe existante par héritage

Utilisez l’héritage public lorsque le nouveau type est véritablement un sous-type de la classe de base et doit pouvoir être utilisé par son interface publique. Une classe dérivée peut ajouter des membres et des fonctions ; elle ne redéfinit pas la classe de base. Avec l’héritage public, les membres publics de la base restent publics et ses membres protégés restent accessibles au code de la classe dérivée. Les membres privés de la base ne sont pas directement accessibles à la classe dérivée. L’héritage privé et l’héritage protégé modifient la manière dont les membres de la base sont exposés aux utilisateurs du type dérivé.

Exécutez cet exemple autonome avec un compilateur C++ :

Le code et les libellés ou sorties affichés dans cette section restent en anglais.

#include <iostream>

class Greeter {
 public:
  void greet() const { std::cout << "Hello from DelftStack!\n"; }
};

class FriendlyGreeter : public Greeter {
 public:
  void greet_twice() const {
    greet();
    greet();
  }
};

int main() {
  FriendlyGreeter greeter;
  greeter.greet_twice();
}
Hello from DelftStack!
Hello from DelftStack!

Ce programme affiche le message de bienvenue deux fois. FriendlyGreeter est une classe de bienvenue spécialisée qui réutilise la fonction publique héritée. Si cette relation de sous-typage n’est pas réelle, l’héritage risque de rendre l’interface trompeuse.

Ajouter des comportements par composition

La composition exprime une relation « possède un » : une classe stocke un objet d’une autre classe et lui délègue certaines tâches. L’interface du type contenu reste ainsi distincte, et la classe qui le contient peut choisir les opérations qu’elle expose. Ici, Report stocke un membre Formatter et lui délègue le rendu ; un rapport possède un formateur, mais n’est pas lui-même un formateur.

Le code et les libellés ou sorties affichés dans cette section restent en anglais.

#include <string>

class Formatter {
 public:
  std::string format(const std::string& text) const {
    return "Report: " + text;
  }
};

class Report {
 public:
  std::string render(const std::string& text) const {
    return formatter_.format(text);
  }

 private:
  Formatter formatter_;
};

int main() {
  Report report;
  return report.render("Summary") == "Report: Summary" ? 0 : 1;
}

La composition repose elle aussi sur des définitions de classes distinctes. Si la classe d’un objet membre est définie dans un fichier d’en-tête, incluez ce fichier et ne définissez la classe qui le contient qu’une seule fois. Ajouter un objet membre ne résout pas une définition en double ailleurs dans la même portée.

Choisir entre héritage et composition

Choisissez l’héritage lorsque le nouveau type doit respecter le contrat du type de base et peut être utilisé sans risque partout où celui-ci est attendu. Choisissez la composition lorsque le nouveau type possède ou utilise un autre objet comme composant, ou ne doit exposer qu’une partie de ses comportements. La composition permet souvent de maintenir des dépendances et des interfaces publiques plus indépendantes ; l’héritage est utile lorsque la substituabilité est intentionnelle.

Aucune de ces approches ne corrige une définition de classe en double accidentelle. Commencez par établir une définition valide pour chaque type. Utilisez ensuite l’héritage ou la composition seulement si la relation de conception souhaitée le justifie.

Vérifier la correction avec un compilateur

Après avoir supprimé ou regroupé la définition en double, compilez l’unité de traduction concernée ainsi que les cibles pertinentes du projet. Pour un fichier source unique nommé main.cpp, une commande courante est g++ -std=c++17 main.cpp -o app ; lancez le programme obtenu avec ./app. L’exemple d’héritage ci-dessus a été compilé et exécuté : il s’est terminé avec le statut 0 et a affiché les deux messages de bienvenue présentés plus haut. Le diagnostic de définition en double est illustratif ; les preuves d’exécution fournies concernent l’exemple d’héritage, et non l’exécution du cas de diagnostic.

Si le diagnostic persiste, examinez les deux emplacements signalés par le compilateur et retracez les fichiers d’en-tête inclus. Si la définition en double n’apparaît que dans une configuration de compilation, comparez ses chemins d’inclusion et ses définitions du préprocesseur. Lorsque vous partagez le diagnostic, gardez son texte sélectionnable : sa formulation et les numéros de ligne dépendent du compilateur et de la compilation.