Partage
  • Partager sur Facebook
  • Partager sur Twitter

membre statique d'une classe non résolu

    27 août 2022 à 17:45:59

    Bonjour à tous,

    je débute en C++ et j'ai un problème avec un singleton. J'ai défini un membre statique privé représentant son instance et il est censé être renvoyé par la méthode statique getInstance() mais pour une raison obscure à la compilation je reçoit cette erreur:

    Erreur	LNK2001	symbole externe non résolu "public: static class SQLDatabase SQLDatabase::sqlDatabaseInstance" (?sqlDatabaseInstance@SQLDatabase@@2V1@A)	
    

    Je vous met le code pour plus de clarté:

    SQLDatabase.h

    #pragma once
    #include "sqlite3.h"
    
    __declspec(dllexport) class SQLDatabase
    {
    public:
    	
    	static SQLDatabase* getInstance();
    	~SQLDatabase();
    private:
    	sqlite3* db;
    	static SQLDatabase sqlDatabaseInstance;
    
    	SQLDatabase();
    };



    SQLDatabase.cpp

    #include "SQLDatabase.h"
    #include "sqlite3.h"
    #include <iostream>
    
    using namespace std;
    
    SQLDatabase* SQLDatabase::getInstance()
    {
        if (sqlDatabaseInstance.db == nullptr)
            if (sqlite3_open("database", &(sqlDatabaseInstance.db)) != SQLITE_OK)
                cout << sqlite3_errmsg(sqlDatabaseInstance.db);
            else
                cout << "Successfuly openned database";
    
        return &sqlDatabaseInstance;
    }
    
    SQLDatabase::~SQLDatabase()
    {
        if(sqlDatabaseInstance.db != nullptr)
            sqlite3_close(sqlDatabaseInstance.db);
    }

    Merci d'avance pour votre réponse,

    SniffierPond

    • Partager sur Facebook
    • Partager sur Twitter
      27 août 2022 à 19:17:43

      Les membres statiques non constexpr d'une classe doivent être définis dans le .cpp, ce que tu n'as pas fait. Quelque chose comme SQLDatabase::sqlDatabaseInstance {};.

      Mais en réalité, ce membre n'a pas besoin d'être dans la classe, il peut se trouver directement dans la fonction. Je ne comprends d'ailleurs pas pourquoi il y a sqlDatabaseInstance et db.

      Mais la plus grande erreur est de vouloir un singleton: ce n'est rien de plus qu'une variable globale en costard. Le flux du code serait beaucoup clair en instanciant la db dans une des fonctions principales et en passant le paramètre à toutes les classes / fonctions qui l'utilisent.

      • Partager sur Facebook
      • Partager sur Twitter
        27 août 2022 à 20:46:01


        jo_link_noir a écrit:

        Les membres statiques non constexpr d'une classe doivent être définis dans le .cpp, ce que tu n'as pas fait. Quelque chose comme SQLDatabase::sqlDatabaseInstance {};.

        Mais en réalité, ce membre n'a pas besoin d'être dans la classe, il peut se trouver directement dans la fonction. Je ne comprends d'ailleurs pas pourquoi il y a sqlDatabaseInstance et db.

        Mais la plus grande erreur est de vouloir un singleton: ce n'est rien de plus qu'une variable globale en costard. Le flux du code serait beaucoup clair en instanciant la db dans une des fonctions principales et en passant le paramètre à toutes les classes / fonctions qui l'utilisent.

        Merci beaucoup pour ta réponse, je vais changer le code pour que ça soit plus clean

        -
        Edité par SniffierPond 27 août 2022 à 20:46:26

        • Partager sur Facebook
        • Partager sur Twitter
          27 août 2022 à 20:46:20

          Salut,

          C'est tout le problème des membres statiques de classes: il ne dépendent d'aucune instance particulière de la classe.  Je m'explique:

          Les membres de classes (et de structures, car la différence entre les deux est minime) "classiques" vont, forcément, appartenir à une instance particulière de la classe.  ainsi, si tu as une classe proche de

          class MaClasse{
          public:
              /*tout ce qui est public, sans intérêt dans le cas présent */
          private:
              int a;
              int b;
              float f;
          };

          les données a, b et f existeront **forcément ** pour chaque variable de type MaClasse que tu vas créer.  En effet, si tu utilise cette classe dans un code proche de

          int main(){
              MaClasse premiere;
              /* ... */
              Maclasse deuxième;
              /* ... */
              MaClase troisiemme;
              /* ... */
          }

          les trois variables (première, deuxieme et troisieme) vont forcément disposer de données a, b et f qui leur sont propres: si tu modifie la donnée a de la variable premiere, cela n'aura aucune espèce d'incidence sur la valeur de la donnée a que l'on trouve dans deuxieme ou dans troisieme.

          Ce n'est pas la situation dans laquelle on se trouve avec les données statiques: Ce sont des données qui "appartiennent" à la classe, mais qui ne dépendent d'aucune instance particulière de la classe. 

          En simplifiant à l'extrême, nous pourrions dire que les données statiques sont en fait des données globales qui se cachent à l'intérieur de la classe

          C'est à dire que, si j'avais une données statique (appelons la s pour savoir de quoi on parle :D ) dans la classe MaClasse, les variables premiere, deuxieme et troisieme pourraient sans aucun problème y accéder (parce que s fait effectivement partie de la classe), mais surtout que ce serait toujours la même donnée qui serait manipulée.

          Autrement dit, après avoir changé la valeur de s à partir de première (ce que je peux faire), si je m'intéresse à la valeur de s à partir de deuxieme ou de troisieme, j'obtiendrai la valeur que j'ai définie à partir de premiere.

          Ce qui est normal, vu que s n'appartient effectivement ni à premiere, ni a deuxieme, ni a troisieme.

          Par contre, cette "non appartenance" à aucune des variables de type MaClasse que je pourrais créer est aussi la base du problème auquel tu es confronté: la donnée s doit exister avant même que l'on ne crée la moindre donnée de la classe.

          Pire encore: cette donnée doit exister "quelque part en mémoire" même si on venait à ne jamais créer de variable de type MaClasse pour la contenir.

          Il va maintenant falloir que l'on se mette d'accord sur certains termes qui seront utilisés, autrement, tu ne comprendras pas ce que je veux t'expliquer.

          En C++ on fait en effet la différence entre la déclaration (d'une donnée, d'une fonction ou d'un type de donnée) et la définition (d'une donnée, d'une fonction ou d'un type de donnée).

          La déclaration permet uniquement au compilateur de savoir que "quelque chose existe".

          Pour un type de donnée, on peut -- par exemple -- faire savoir au compilateur que "la classe MaClase" existe sans entrer dans les détails avec ce que l'on appelle une déclaration anticipée qui prendra la forme de

          class MaClasse;

          Et tant que le compilateur n'a pas besoin "d'en savoir plus" sur cette classe (par exemple, la quantité d'espace mémoire nécessaire pour stocker une variable de type MaClasse), il pourra se contenter de savoir que la classe MaClasse  existe, que ce n'est pas un terme qui "sort de nulle part".

          De la même manière, nous pouvons déclarer une fonction avec un code proche de

          TypeDeRetour nomDeLaFonction( /* liste des paramètres éventuels */ );

          qui va simplement dire au compilateur que la fonction nomDeLaFonction (qui prend éventuellement les paramètres indiqués) et qui renvoie une donnée de type TypeDeRetour existe bel et bien, ce qui permettra au compilateur, lorsqu'il croise l'appel à cette fonction "quelque part dans le code", de s'assurer qu'on y fait appel "de manière correcte".

          Enfin, pour ce qui est des données en elles-même, nous parlerons d'une déclaration lorsque le code que l'on écrit n'a pas pour "effet immédiat" de réserver "quelque part en mémoire" un espace suffisant que pour pouvoir stocker la variable. Je reviendrai là dessus dans quelques lignes ;)

          La définition quant à elle va systématiquement provoquer la réservation "quelque part en mémoire" d'un espace suffisant pour stocker le contenu que l'on vient de définir.

          Pour ce qui est des fonctions, nous parlerons aussi bien de définition que d'implémentation (il y a une légère différence, mais peu de gens y font attention), et elle s'obtient en... donnant le corps de la fonction entre les accolades '{' et '}', sous une forme proche de

          /* ceci est une déclaration de fonction */
          TypeDeRetour nomDeLaFonction( /* liste des paramètres éventuels */ );
          /* et ceci est la définition (ou l'implémentation) de 
           * la même fonction
           */
          TypeDeRetour nomDeLaFonction( /* liste des paramètres éventuels */ )
          {
              /* tout ce qui doit être fait pour obtenir le résultat
               * espéré
               */
          }

          De la même manière, nous parlons de définition d'une classe (ou d'un type personnalisé, de manière générale) lorsque nous établissons le contenu, le corps de cette classe, également entre les accolades '{' et '}', sous une forme proche de

          /* Ceci est la déclaration d'un type de donnée nommé
           * MaClasse
           */
          
          class MaClasse;
          /* et ceci est la définition du type en question */
          class MaClasse{
              /* Tout ce qui apparait ici donne "un corps" à ce
               * type de donnée, indépendamment de l'accessibilité
               * dans laquelle c'est déclaré
               */
          public:
              /* il peut y avoir une fonction membre */
              Type foo(/*paramètres éventuels*/);
          private:
              /* il peut y avoir des données particulières */
              int a;
              int b;
              float f;
          };

          Et grâce à cette définition, le compilateur est désormais en mesure de savoir exactement quelle sera la taille de l'espace mémoire qu'il devra réservé pour être en mesure de stocker une donnée de type MaClasse.

          Le truc, c'est que, tant que l'on ne crée pas effectivement une donnée de type MaClasse, il n'y a ... aucune raison de réserver "quelque part en mémoire" l'espace nécessaire pour stocker les données a, b et f.

          Cela peut sembler bizarre, mais ce que l'on observe ici n'est, bel et bien qu'une déclaration des données a, b et f, vu qu'elles ne prendront effectivement de la place en mémoire qu'une fois ... qu'on aura créé une donnée de type MaClasse.

          A l'heure actuelle, et jusqu'à ce que l'on décide effectivement de créer une donnée de type MaClasse "quelque part dans le code", c'est bel et bien de dire que "il existe une donnée de type int nommée a, une donnée de type int nommée b et une donnée de type float nommée f dans la classe MaClasse" ;)

          De même, si tu y regarde d'un peu plus près, tu te rendra compte que je n'ai fourni que la déclaration de la fonction, vu que je n'en ai pas donné le corps. Et de la même manière, jusqu'à ce que l'on décide de créer une donnée de type MaClasse, le seul résultat auquel on s'attend est ... de signaler au compilateur qu'il existe, dans la classe MaClasse, une fonction nommée foo qui prend tels types de paramètres et qui renvoie une donnée de type Type et à laquelle on peut faire appel "depuis n'importe quelle donnée de type MaClasse".

          Il faudra quand même fournir la définition (ou l'implémentation)  de cette fonction "quelque part dans le code" pour que le compilateur puisse générer le code correspondant ;)

          Bon, j'espère que j'ai été assez clair que pour te permettre de comprendre la différence.  Si ce n'est pas le cas, n'hésite pas à poser les questions qui te turlupinent ;)

          Car il est temps de revenir à notre problème de base qui est, faut il le rappeler, qu'une donnée statique de classe ne dépend d'aucune instance particulière de la classe en question.

          Et comme je viens de l'expliquer, la définition d'une classe ne contient (à la base) que des déclarations.

          C'est valables pour les fonctions membres, pour les données "classiques" et bien sur aussi ... pour les données statiques de la classe.

          Et c'est ce qui pose tout le problème des données statiques: le fait de créer une variable du type de notre classe va forcément réserver l'espace mémoire pour représenter l'ensemble des données classiques de notre classe, mais comme la donnée statique n'appartient à aucune instance particulière de la classe.

          De plus, je te rappelle ce que j'ai dit plus haut, la donnée statique de la classe est également sensée pouvoir exister même si aucune instance de la classe n'est créée.

          On ne peut donc simplement pas espérer que l'espace mémoire permettant de la représenter en mémoire soit géré par une des instances de la classe, vu que, a priori, il peut "tout aussi bien" ne pas y en avoir.

          Ce qu'il faut donc faire, c'est ... fournir au compilateur "une bonne raison" pour ... réserver "quelque part en mémoire" un espace suffisant permettant de représenter la donnée en question.

          Et pour y arriver, il n'y a pas trente-six solution: il faut placer "quelque part dans le code" la définition de cette donnée.  Il faudra juste veiller à ce que cette définition ne fasse absolument pas partie d'une fonction (qui pourrait au demeurant ne jamais être appelée)

          Maintenant que nous avons compris le problème, il est sans doute temps de s'intéresser à la solution dans ta situation particulière.

          Il semble évident à voir ton code que tu cherches à utiliser le patron de conception appelé singleton.

          Autant le dire tout de suite, pour que ce soit fait, ce n'est très certainement pas la meilleur décision conceptuelle que tu puisse prendre. Cependant, je garderai mes remarques sur le sujet pour plus tard, si cela t'intéresse, car ma réponse a déjà suffisamment l'allure d'un roman ;)

          Par contre, tu te fais beaucoup de mal pour rien dans la manière dont tu met ton singleton en oeuvre, car un code "aussi simple" que ceci pourrait suffir:

          Le fichier d'en-tête:

          /* je supprime le __declspec(dllexport) car il n'est valable
           * qu'à la compilation d'une dll, on peut en parler
           * plus tard 
           */
          class SQLDatabase
          {
          public:
              /* il est préférable d'utiliser les références à
               * chaque fois que c'est possible et de ne garder
               * les pointeurs que pour les cas où on n'a vraiment
               * pas le choix
               * l'utilisation d'une référence est possible ici, 
               * autant en profiter 
               */ 
              static SQLDatabase & getInstance();
              /* et juste une fonction pour voir que tout fonctionne */
              void foo();
          private:
              /* personne ne peut ni créer ni détruire une instance
               * de SQLDatabase sans passer par la fonction getInstance()
               * mais cette fonction a parfaitement accès 
               * à tout ce qui est privé, y compris le constructeur
               * et le destructeur
               */
               SQLDatabase();
               ~SQLDatabase();
               /* on refuse également la copie et l'affectation
                */
               
               SQLDatabase(SQLDatabase const &) = delete;
               SQLDatabase& operator = (SQLDatabase const &) = delete;
               */
               
              sqlite3* db;
              /* nul besoin de pointeur (static qui plus est)
               * sur l'instance de SQLDatabase (tu verra pourquoi plus loin)
               */
          };

          Il y a beaucoup de commentaires, mais, si tu les retire (parce que tu as compris ce qu'ils t'explique), tu verras qu'il est basiquement plus simple que le tien ;)

          Le fichier d'implémentation maintenant.  Lui aussi, il va contenir un maximum de commentaires qui pourraient être évités.  N'hésite pas à les (re)lire, tu apprendras sans doute énormément ;)

          #include "SQLDatabase.h"
          #include "sqlite3.h"
          #include <iostream>
          #include <stdexcept>
          
          /* Dans l'idéal, la directive using namespace std; ne devrait pas
           * être employée 
           */
          SQLDatabase& SQLDatabase::getInstance()
          {
              /* il est possible de déclarer une variable statique
               * dans une fonction.
               * Cette variable sera alors créée la première fois
               * que la fonction est appelée et existera 
               * jusqu'à ce que l'exécution s'achève.
               *
               * nul besoin de passer par un pointeur 
               */
              static SQLDatabase instance; // le constructeur 
                                           // s'occupe de tout 
                  if (sqlite3_open("database", &db)) != SQLITE_OK)
                      cout << sqlite3_errmsg(sqlDatabaseInstance.db);
           
              return &sqlDatabaseInstance;
          }
          /* la "fonction de test"
           */
          void SQLDatabase::foo(){
              std::cout<

          Note que nous pourrions également créer une exception perso (comme SqlExecption ou autre) au cas où il faudrait s'attendre à ce que certains problèmes n'ayant rien à voir avec la base de données pourraient survenir...  Mais ce n'est ici qu'un détail ;)

          L'utilisation de ce code (je t'assure que si tu vire les commentaire, il est beaucoup plus simple que le tien, ne te laisse pas avoir par la fausse taille de mon code :D ) ressemblerait au final à quelque chose comme

          #include "SQLDatabase.h"
          
          int main(){
              /* donnons nous une possibilité de récupérer l'exception
               */
              try(){
                  /* j'utilise des possibilités apparues depuis 2011,
                   * ca facilite la vie
                   */
                  auto & instance = SQLDatabase::getInstance();
                  instance.foo();
          
                  /* si on a besoin d'une autre variable, on 
                   * se rend compte que le constructeur n'est pas
                   * rappelé (c'est donc la même instance de
                   * SQLDatabase qui est utilisée 
                   */
                  auto & autreInstance = QLDatabase::getInstance();
                  autreInstance.foo();
              }
              catch(std::runtime_error & e){
                  /* on a récupéré le problème, mais on n'a 
                   * aucune solution pour le moment, on abandonne
                   * on affiche le problème puis on abandonne ici
                   */
                  std::cout<<e.what()<<"\n";
                  exit(1);
              }
              /* tout s'est bien passé, on le signale au système
               * d'exploitation
               */
              return 0;
          }

          Je vais m'arrêter là, le temps que tu digère tout cela et que tu fasse tes essais... n'hésite pas à pser la moindre question qui pourrait te venir à l'esprit concernant ce roman... Pardon, cette intervention ;)

          • Partager sur Facebook
          • Partager sur Twitter
          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
            27 août 2022 à 21:28:03

            Merci énormément d'avoir pris le temps d'écrire cette réponse (très) détaillée. Tout ce que tu m'as donné m'est hyper utile. En fait j'ai beau essayer je n'ai aucune bonne pratique de programmation ce qui fait que mon code est moche, peu fiable et robuste. Je crois que je vais aller jetter un coup d'oeil à ton livre, ça m'a l'air intéressant.

            EDIT: Par contre j'ai dû mettre des "=default" devant le constructeur et le destructeur autrement le compilateur ne les trouvait pas.

            -
            Edité par SniffierPond 28 août 2022 à 13:21:01

            • Partager sur Facebook
            • Partager sur Twitter

            membre statique d'une classe non résolu

            × 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.
            • Editeur
            • Markdown