Je vais utilisé une base de données et je voudrais bien gérer mes table via des objets.
Tout ces objet (Personne,permission j'essaye une relation 1 à n) Hérite d'une classe db dans laquel est défini des méthode virtuel pure car je voudrais ne pas oublier qu'il faut réimplementer certaine méthode.
Les définitions des template doivent être placés dans le header pas dans un fichier source. Donc template<class C> Db<C>::Db(QString from) { ...doit être dans db.h
De manière générale, c'est toujours une très mauvaise idée de vouloir grouper la programmation générique (l'utilisation de template) et le polymorphisme d'inclusion (les fonctions virtuelles).
La raison est toute simple : le type des paramètres template est évalué exclusivement à la compilation, alors que le polymorphisme d'inclusion est évalué exclusivement ... à l'exécution.
je m'explique, mais, pour la compréhension, je vais partir d'une fonction template toute simple à laquelle je vais donner la forme de
La première chose à comprendre, c'est que ce code va en réalité dire au compilateur quelque chose comme
Je ne sais pas encore quel sera le type réel du paramètre nommé value lorsque l'on fera appel à la fonction printValue, mais voici la manière dont value sera utilisé
(et, bien sur, l'implémentation ici indiquera dans le cas présent qu'il faut envoyer le contenu de value vers la sortie standard, et faire suivre cette valeur d'un retour à la ligne).
La deuxième chose importante, c'est que le compilateur ne va pas générer le code binaire (qui sera utilisé par l'application) pour cette fonction "directement", comme il le fait pour les fonctions "normales" (non template) pour une raison toute simple : il ne sait pas, au moment où il lit ce code, quel genre de donnée il devra envoyer vers la sortie standard.
L'intérêt de cette fonction tient essentiellement dans le fait qu'elle pourra fonctionner avec tous les types de données pour lequel il l'opérateur << a été défini.
Cela fonctionne donc parfaitement pour les types primitifs (int, char, float, et les autres), mais aussi pour tous les types définis par l'utilisation (les structures et les classes) pour lesquels l'opérateur << a été défini.
Ainsi, si, par la suite, j'écris un programme proche de
#include <iostream>
#include <string>
template <typename T>
void printValue(T value){
std::cout<<value<<"\n";
}
struct Point{
int x;
int y;
};
/* ah... evidemment, l'opérateur << n'existe pas encore pour
* la structure Point... corrigeons cela tout de suite
*/
std::ostream & operator<< (std::ostream & ofs, Point const & point){
ofs<<"un point x :"<<point.x<<", y: "<<point.y;
return ofs;
}
int main(){
int unEntier{5};
printValue(unEntier);
float pi{3.1415926};
printValue(pi);
std::string myName{"koala01"};
printValue(myName);
Point unPoint{3,10};
printValue(unPoint);
}
(j'ai tout mis dans le même fichier, pour que les numéros de lignes soient identiques )
Ce qui va se passer, c'est que, quand le compilateur va se rendre compte, en traitant la ligne 20 que je fait appel à printValue en lui transmettant un entier, il va ... "enfin" générer le code binaire de printValue pour traiter les entiers.
De même,
lorsqu'il va se rendre compte à la ligne 22 que j'appelle la fonction avec un float, il va générer le code binaire correspondant pour ... les float,
lorsqu'il va se rendre compte à la ligne 24 que j'appelle la fonction avec une chaine de caractères de type std::string, il va générer le code binaire correspondant pour ... les chaines de caractères de type std::string
lorsqu'il va se rendre compte à la ligne 24 que j'appelle la fonction avec une structure de type Point, il va générer le code binaire correspondant pour ... les structures de type Point
Et il sera à chaque fois content, parce que l'opérateur << existe bel et bien pour chacun de ces types de données.
Si je rajoutais une autre structure de donnée, par exemple
main.cpp: In instantiation of 'void printValue(T) [with T = Personne]':
main.cpp:34:24: required from here
main.cpp:5:14: error: no match for 'operator<<' (operand types are 'std::ostream' {aka 'std::basic_ostream<char>'} and 'Personne')
5 | std::cout<<value<<"\n";
| ~~~~~~~~~^~~~~~~
(suivi de toute une liste de choses qui découlent de ce problème :P) parce qu'il se rendra compte, en voulant générer le code binaire exécutable de la fonction printValue, qu'il ne sait pas comment faire passer la structure Personne par l'opérateur << .
Les fonctions virtuelles fonctionnent de manière tout à fait différente, car la résolution du type réel est effectuée à l'exécution (au moment où le programme est effectivement lancé par l'utilisateur).
Le principe sur lequel elles se basent est le principe de la substituabilité: certains types de donnée peuvent être considérés comme des "évolutions", ou plutôt comme des "spécialisations" de types de données plus "génériques".
Ainsi, une voiture, une moto, un camion et, pourquoi pas, un char à voile peuvent tous être considérés comme la "spécialisation" du type de donnée "véhicule" parce que ce sont -- effectivement -- autant de véhicules différents.
Les fonctions virtuelles nous permettant d'indiquer au compilateur que le comportement de la fonction (comme "tourner(à gauche ou à droite)"), la manière dont la fonction obtiendra le résultat voulu devra s'adapter au type de véhicule qui sera effectivement utilisé.
En effet, tous les véhicules sont capable de tourner à gauche ou à droite. Et, pourtant, ils ne s'y prennent pas de la même manière:
un camion devra tourner "large" pour éviter d'aller se frotter à l'intérieur du tournant
une voiture pourra tourner "normalement"
une moto utilisera sans doute le "contre braquage" (on pousse le guidon à droite pour tourner à gauche!!!)
le conducteur d'un char à voile devra faire attention à ne pas se faire assommer par la baume de la voile
Mais, le comportement adapté devra être choisi à l'exécution, parce que l'on aura décidé de ne retenir de notre moto, de notre camion, de notre voiture ou de notre char à voile que le fait qu'il s'agit ... d'un véhicule.
Ce qui nous donnera un code sans doute proche de
class Vehicule{
public:
/* tout ce dont un véhicule est capable
* dont le fait de tourner
*/
virtual void tourner(Sens sens) = 0; // (*)
};
class Camion{
public:
void tourner(Sens sens) override; // on définit le comportement
// spécifique pour le camion
};
class Voiture{
public:
void tourner(Sens sens) override; // on définit le comportement
// spécifique pour la voiture
};
class Moto{
public:
void tourner(Sens sens) override; // on définit le comportement
// spécifique pour la moto
};
class CharAVoile{
public:
void tourner(Sens sens) override; // on définit le comportement
// spécifique pour le char à voile
};
int main(){
/* on se fout pas mal de savoir quel est le type
* de véhicules que l'on devra gérer.
*
* on sait que ce sont des véhicules, et cela nous suffit
*/
std::vector<std::unique_ptr<Vehicule>> lesVehicules;
/* on dispose d'une fonction creerVehicule qui nous crée
* selon le cas, un camion, une voiture, une moto ou un
* char à voile ...
*/
lesVehicule.push_back(creerVehicule(/*ceci sera un camion */) ):
lesVehicule.push_back(creerVehicule(/*ceci sera une voiture */) ):
lesVehicule.push_back(creerVehicule(/*ceci sera une moto */) ):
lesVehicule.push_back(creerVehicule(/*ceci sera un char à voile */) ):
/* on a des véhicules, on veut les faire tourner */
for(auto & it : lesVehicules){
it->tourner(gauche); //le polymorphisme agit ici
//car c'est le comportement
// spécifique au type réel
// de véhicule qui sera appelé
}
}
(*) Tu remarqueras que, au niveau de la "classe de base" (la classe Vehicule), la fonction tourner est déclarée comme étant "virtuelle pure" (c'est le "= 0" qui l'indique).
Cela nous permet de dire au compilateur quelque chose comme
Je sais que tu n'aimes pas le vide, et que toutes les fonctions doivent être implémentées, mais, à l'heure actuelle, je ne dispose pas des données suffisantes que pour te fournir un comportement adéquat pour cette fonction.
Le compilateur va accepter cette situation (il n'a pas vraiment le choix, me diras-tu ), cependant, il va néanmoins poser une restriction importante sur l'utilisation de la classe Vehicule.
Il va, en effet, accepter que l'on désigne des données comme étant "des véhicules" (c'est ce que font les lignes lesVehicule.push_back(...)), mais uniquement pour les classes dérivées de Véhicule (Camion, Moto, Voiture, CharAVoile) qui auront fourni le comportement de la fonction tourner.
Par contre, si tu venais à essayer de créer une instance de la classe Vehicule, il t'engueulerait parce qu'il n'autorise pas la création d'un Vehicule (sans autre précision), du fait de la présence de la fonction virtuelle pure.
On se rend compte qu'il n'y a -- a priori -- pas beaucoup de raisons pour "mélanger" les deux aspects: il y a -- d'un coté -- les décisions qui doivent être prises à la compilation, et qui utilisent les templates, et, de l'autre coté (mais vraiment à l'opposé du spectre, oserais-je dire), il y a les décision qui doivent être prise à l'exécution du programme, et qui utilisent les fonctions virtuelles.
Au final, si je n'ai qu'un seul conseil à te donner, c'est de clairement faire la distinction entre ce qui se passe, les décisions qui sont prises à la compilation et ce qui se passe à l'exécution.
Il y a -- pour tout dire -- parfaitement moyen de mélanger les deux. Cependant, cela nécessite une maîtrise telle du C++ que tu n'as clairement pas les connaissances nécessaires pour t'y frotter.
Commence donc par faire des choses simples, mais qui fonctionnent (correctement): utilise les templates si tu veux utiliser les templates, mais n'essaye pas d'y ajouter le polymorphisme d'inclusion (les fonctions virtuelles) ou, inversement, utilise le polymorphisme d'incusion (les fonctions virtuelles) si tu veux utiliser le polymorphisme, mais n'essaye pas d'y ajouter les templates.
"Plus tard", quand tu maîtrisera de nombreux concepts dont tu n'as sans doute encore jamais entendu parler, il sera toujours temps d'essayer de faire des choses plus complexes (en "mixant" les templates avec le polymorphisme d'inclusion, par exemple). Mais tu te rendras très rapidement compte si tes connaissances te permettent de t'y frotter ou non: si as les connaissances nécessaire, un tel mixte te semblera presque naturel, et tu n'auras pas de raison de ne pas y avoir recours.
Si, par contre, un tel mixte te pose des problèmes auxquels tu es incapable d'apporter une réponse par toi-même, ce sera qu'il est simplement "trop tôt" pour essayer de te frotter à ce genre de problème, et qu'il est sans doute préférable d'essayer une solution "moins complexe"
- Edité par koala01 27 juillet 2020 à 12:57:51
Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
Méthode virtuelle pure problème de linker
× Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
× Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
En recherche d'emploi.
En recherche d'emploi.