Partage
  • Partager sur Facebook
  • Partager sur Twitter

Somme de matrice template

Additionner 2 matrices de types différents

Sujet résolu
    23 novembre 2020 à 11:11:52

    Bonjour, toujours en train de travailler sur mon petit exo qui était de faire une classe matrice en respectant le principe de RAII, c'est un peu en train de se transformer en exercice sur les template vu que je galère un petit peu avec.

    Pour l'historique, voir ce sujet où on m'a aidé à concevoir cette classe matriceRAII : https://openclassrooms.com/forum/sujet/surcharge-operateur-9

    Ma matrice est très proche de la proposition finale de lmghs à la fin du post

    Actuellement j'ai surchargé les opérateurs + et += pour faire une somme basique sur deux matrices. Ca fonctionne très bien mais je trouve ça un petit peu limitant puisque je ne peux sommer que deux matrices qui sont de meme type. Cependant, il serait tout à fait légitime de pouvoir sommer une matrice d'int avec une matrice de double, ou avec des unsigned_int ou des float ect ....

    Le code que j'ai actuellement pour mes opérateurs : 

    template<typename T>
    struct Alocator { // Gestion de la mémoire et quelques opérateurs, cf post précédent }
    // Classe Matrice suivant le principe RAII
    template<typename T>
    class Matrice
    {
    public:
    	Matrice(std::size_t ligne, std::size_t col)
    		: lines_(ligne)
    		, cols_(col)
    		, storage(ligne* col, {})
    	{	}
    		
    
    	Matrice(std::size_t ligne, std::size_t col, T def)
    		: lines_(ligne)
    		, cols_(col)
    		, storage(ligne * col, def)
    	{	}
    
    	Matrice(Matrice const& mat) = default;
    	Matrice(Matrice&& mat) = default;
    
    	~Matrice() = default;
    
    	friend void swap(Matrice& that, Matrice& mat) {
    		using std::swap;
    
    		swap(that.lines_, mat.lines_);
    		swap(that.cols_, mat.cols_);
    		swap(that.storage, mat.storage);
    	}
    
    	// -------- Operator overloading --------
    	Matrice& operator=(Matrice mat) {
    		swap(*this, mat);
    		return *this;
    	}
    
    	Matrice& operator+=(Matrice<T> const& mat) {
    		assert((this->lines_ == mat.lines_ && this->cols_ == mat.cols_) && "Matrice should have the same size.");
    
    		for (std::size_t i = 0; i < lines_; i++)
    			for (std::size_t j = 0; j < cols_; j++)
    				storage[i * cols_ + j] += mat.storage[i * cols_ + j];
    
    		return *this;
    	}
    
    	template<typename T>
    	Matrice& operator+(Matrice<T> const& mat) {
    		return *this += mat;
    	}
    
    	// --------------------------------------
    
    
    	void display() {
    		for(std::size_t i = 0; i < lines_; i++) {
    			for (std::size_t j = 0; j < cols_; j++)
    				std::cout << storage[i * cols_ + j] << "  ";
    			std::cout << '\n';
    		}
    		std::cout << "----------------" << std::endl;
    	}
    
    private:
    	std::size_t lines_;
    	std::size_t cols_;
    
    	Alocator<T> storage;
    };

    Voici ce que j'ai essayé pour sommer deux matrices de type différent, c'est une fonction déclarée public dans ma classe Matrice :

    template <typename T, typename K>
    friend Matrice testSum(Matrice<T> const& that, Matrice<K> const& other) {
    	assert((that.lines_ == other.lines_ && that.cols_ == other.cols_) && "Matrice should have the same size.");
    
    	Matrice<T> ret{ that.lines_, that.cols_ };
    
    	for (std::size_t i = 0; i < that.lines_; i++)
    		for (std::size_t j = 0; j < that.cols_; j++)
    			ret.storage[i * ret.cols_ + j] = that.storage[i * ret.cols_ + j] + other.storage[i * ret.cols_ + j];
    
    	return ret;
    }

    Et je le test dans le main comme ça 

    int main()
    {   
    
        Matrice<int> mat{ 5,5,1 };
        mat.display();
    
        Matrice<double> second{ 5,5,1.2 };
        second.display();   
    
        auto ret = testSum(mat, second);
        ret.display();
        
    
        return 0;    
    }


    Je m'attendais à ce que ça "marche", à savoir que la somme des deux matrices se fasse mais que le type de retour sois Matrice<int> et donc que le contenu de la matrice<double> second sois implicitement casté en int.

    Au lieu de ça ça ne marche pas du tout, j'ai l'erreur de compilation sur l'appel de testSum dans le main :

    E0308 : Plusieurs instances de "testSum" correspondent à la liste d'arguments :
    Modèle de fonction "Matrice<double> testSum(liste de mes arguments)
    Modèle de fonction "Matrice<int> testSum(liste de mes arguments)

    J'ai aussi essayé avec ce prototype de fonction :

    template <typename T, typename K>
    friend auto testSum(Matrice<T> const& that, Matrice<K> const& other) {
        // Même contenu qu'avant
    }

    Mais j'obtient l'erreur sur la déclaration de ma fonction

    C2995 : 'auto testSume(const Matrice<T> &, const Matrice<K> &)' : modèle MatriceRAII de fonction déjà défini


    Donc premier point à résoudre : Comment est-il possible d'effectuer la somme de deux matrices de type différent ?

    Deuxième point : Comment est-il possible de déduire le type de retour que je veux ? Si je somme Matrice<int> et Matrice<double> le retour doit être Matrice<double> pour éviter toutes pertes d'info lors d'un cast double vers int.

    Je pensais faire un test dans la fonction du genre : 

    if(sizeof(t) >= sizeof(k)
        Matrice<T> ret = t+k
    else
        Matrice<K> ret = k+t

    avec un opérateur + qui renvoit une matrice du même type que l'opérande de gauche ou un truc du genre mais bon, comme je sais pas résoudre le point 1 j'ai pas pu tester cette idée du tout 

    Merci d'avance pour votre aide :)

    -
    Edité par ThibaultVnt 23 novembre 2020 à 11:24:41

    • Partager sur Facebook
    • Partager sur Twitter
      23 novembre 2020 à 11:36:26

      Au moment où tu crée ret, il faut que le type soit déjà du bon type, tu ne peut pas simplement mettre T, il faut le type commun entre T et K.

      Matrix<std::common_type_t<T, K>> ret

      https://en.cppreference.com/w/cpp/types/common_type

      • Partager sur Facebook
      • Partager sur Twitter
        23 novembre 2020 à 12:34:11

        Merci pour ta réponse, en lisant la doc ça semble en effet tout à fait correspondre à ce dont j'ai besoin mais je n'arrive visiblement pas à l'utiliser correctement.

        template <typename T, typename K>
        	friend Matrice<typename std::common_type_t < T, K>::type> testSum(const Matrice<T>& that, const Matrice<K>& other) {
        		assert((that.lines_ == other.lines_ && that.cols_ == other.cols_) && "Matrice should have the same size.");
        
        		Matrice<typename std::common_type_t < T, K>::type> ret{ that.lines_, that.cols_ };
        
        		for (std::size_t i = 0; i < that.lines_; i++)
        			for (std::size_t j = 0; j < that.cols_; j++)
        				ret.storage[i * ret.cols_ + j] = that.storage[i * ret.cols_ + j] + other.storage[i * ret.cols_ + j];
        
        		return ret;
        	}

        Ici le type de retour est donc indiqué dans le prototype de la fonction et je devrais construire ret avec le bon type pour la matrice.

        Cependant je garde l'erreur sur le prototype de la fonction

        C2995	'Matrice<std::common_type_t<_Ty1,_Ty2>::type> testSum(const Matrice<T> &,const Matrice<K> &)' : modèle de fonction déjà défini	MatriceRAII

        Et sur l'appel de la fonction dans le main 

        Matrice<int> mat{ 5,5,1 };
        mat.display();
        
        Matrice<double> second{ 5,5,10 };
        second.display();   
        
        auto ret = testSum(mat, second);    
        ret.display();

        J'ai deux messages d'erreurs disant :

        Erreur	C2672	'testSum' : fonction correspondante surchargée introuvable	MatriceRAII
        
        C2893	La spécialisation du modèle de fonction 'Matrice<std::common_type_t<_Ty1,_Ty2>::type> testSum(const Matrice<T> &,const Matrice<K> &)' a échoué	MatriceRAII

        J'arrive sans soucis à compiler l'exemple donné en bas de page de cppReference mais je ne vois pas la différence entre ce que j'écris et l'exemple 


        EDIT : J'ai utilisé std::common_type_t comme tu m'avais dit et je viens de voir que la doc utilise std::common_type

        J'ai modifié la fonction pour utiliser std::common_type à la place.

        Je n'ai plus les deux erreurs sur l'appel de la fonction dans le main mais je garde encore et toujours l'erreur 

        C2995	'Matrice<std::common_type<_Ty1,_Ty2>::type> testSum(const Matrice<T> &,const Matrice<K> &)' : modèle de fonction déjà défini	MatriceRAII

        Ce que je ne comprend vraiment pas puisque je ne défini qu'une seule fois cette fonction, il n'y a aucune autre fonction avec le même nom dans tous mes fichiers

        -
        Edité par ThibaultVnt 23 novembre 2020 à 12:44:58

        • Partager sur Facebook
        • Partager sur Twitter
          23 novembre 2020 à 12:45:58

          La différence se situe sur le préfixe _t qui est un alias vers la version sans préfixe:

          template<class... ts> XXX_t = typename XXX<ts...>::type;

          Donc soit on utlise _t, soit typename ...::type, mais pas les 2.

          Concernant l'erreur en elle-même sur testSum, je ne la comprends pas, mais le typename T du template de la fonction est en conflit avec celui de la classe, il faut le renommer.

          -
          Edité par jo_link_noir 23 novembre 2020 à 12:46:46

          • Partager sur Facebook
          • Partager sur Twitter
            23 novembre 2020 à 14:30:43

            Ok alors j'ai compris dans quel cas ça marche et dans quel cas ça ne marche pas, mais le résultat ne me satisfait pas du tout ...

            Ca ne marche pas si ma fonction testSum est une fonction friend de la classe Matrice : 

            class Matrice
            {
            public:
            	Matrice(std::size_t ligne, std::size_t col)
            		: lines_(ligne)
            		, cols_(col)
            		, storage(ligne* col, {})
            	{	}
            
            
                    template<typename T, typename K>
            	friend Matrice<std::common_type_t < T, K>> testSum(const Matrice<T>& that, const Matrice<K>& other) {
            		assert((that.lines_ == other.lines_ && that.cols_ == other.cols_) && "Matrice should have the same size.");
            
            		Matrice<std::common_type_t < T, K>> ret{ that.lines_, that.cols_ };
            
            		for (std::size_t i = 0; i < that.lines_; i++)
            			for (std::size_t j = 0; j < that.cols_; j++)
            				ret.storage[i * ret.cols_ + j] = that.storage[i * ret.cols_ + j] + other.storage[i * ret.cols_ + j];
            
            		return ret;
            	}
            
            private:
            	std::size_t lines_;
            	std::size_t cols_;
            
            	Alocator<T> storage;
            }
            
            
            // Dans le main :
            
            int main()
            {   
                
                Matrice<int> mat{ 5,5,1 };
                mat.display();
            
                Matrice<double> second{ 5,5,10 };
                second.display();   
            
                auto ret = testSum(mat, second);    
                ret.display();
            
                return 0;    
            }
            

            Le code ci dessus ne compile pas et provoquera l'erreur :

            C2995   'Matrice<std::common_type<_Ty1,_Ty2>::type> testSum(const Matrice<T> &,const Matrice<K> &)' : modèle de fonction déjà défini   MatriceRAII
            


            Par contre si je déclare testSum comme une fonction libre, cela fonctionne parfaitement

            template<typename T>
            class Matrice
            {
            public:
            	std::size_t lines_;
            	std::size_t cols_;
            
            	Alocator<T> storage;
            
            
            	Matrice(std::size_t ligne, std::size_t col, T def)
            		: lines_(ligne)
            		, cols_(col)
            		, storage(ligne* col, def)
            	{	}
            
            }
            
            // dans le main.cpp
            
            template<typename T, typename K>
            Matrice<std::common_type_t < T, K>> testSum(const Matrice<T>& that, const Matrice<K>& other) {
                assert((that.lines_ == other.lines_ && that.cols_ == other.cols_) && "Matrice should have the same size.");
            
                Matrice<std::common_type_t < T, K>> ret{ that.lines_, that.cols_ };
            
                for (std::size_t i = 0; i < that.lines_; i++)
                    for (std::size_t j = 0; j < that.cols_; j++)
                        ret.storage[i * ret.cols_ + j] = that.storage[i * ret.cols_ + j] + other.storage[i * ret.cols_ + j];
            
                return ret;
            }
            
            
            int main()
            {   
                
                Matrice<int> mat{ 5,5,1 };
                mat.display();
            
                Matrice<double> second{ 5,5,10.2 };
                second.display();   
            
                auto ret = testSum(mat, second);    
                ret.display();
            
                return 0;    
            }

            Tu remarqueras que dans les deux cas, la synthaxe de ma fonction testSum est parfaitement indentique. Seule différence est que je suis passé d'une fonction friend à une fonction libre. Mais pour que je puisse manipuler les valeurs des matrice, je dois avoir accès à leur storage qui est un membre privé.

            Donc là j'ai fait vite et j'ai tout mis en public comme un crado et c'est pas acceptable mais implémenter un accesseur sur mes trois variables privées ne me semble pas vraiment mieux. J'ai suffisamment vu de commentaire de gbdivers ou autre vétéran du forum répéter en boucle que les get/set violaient le principe d'encapsulation pour qu'une alarme retentisse dans ma tête au moment où j'écris ces mots

            J'ai bien pensé à faire de testSUm une fonction static, elle aurait accès aux membres privés et je pourrais l'utiliser facilement dans le main ... Mais problème, comme ma classe Matrice prend un template, je suis obliger de préciser un type par default par exemple : 

            auto ret = Matrice<int>::testSum(mat, second); 

            Et dans ce cas là ça ne compile pas non plus, j'ai des messages d'erreurs disant que testSum n'a pas accès aux membres privés de Matrice<double>



            -
            Edité par ThibaultVnt 23 novembre 2020 à 14:31:15

            • Partager sur Facebook
            • Partager sur Twitter
              23 novembre 2020 à 16:20:03

              Pour l'erreur elle-même, voir mon précédent message.

              > Donc là j'ai fait vite et j'ai tout mis en public comme un crado et c'est pas acceptable mais implémenter un accesseur sur mes trois variables privées ne me semble pas vraiment mieux. J'ai suffisamment vu de commentaire de gbdivers ou autre vétéran du forum répéter en boucle que les get/set violaient le principe d'encapsulation pour qu'une alarme retentisse dans ma tête au moment où j'écris ces mots.

              À quoi servirait une classe Matrice si tu ne peux ni accéder aux éléments, ni les modifier ? Tu ne vas quand même pas ajouter une fonction amie à chaque algorithme qui vont les manipuler ‽ Ce qui ne tient pas la route en passant puisque comme tu le constates, 2 matrices de types différentes ne peuvent pas accéder aux membres privés l'une de l'autre.

              Le but de la classe Matrice est d’abstraire la représentation mémoire (qui est délégué à Storage (Allocator¹ pour toi)), de fournir les opérateurs mathématiques classiques (+, -, etc) et surtout un moyen de lire et écrire une valeur dans la matrice, sinon on ne peut rien en faire. Tu remarqueras que dans l'implémentation de @lmghs, la classe matrice possède 2 fonctions operator()(std::size_t l, std::size_t c) qui ne sont ni plus ni moins qu'un getter et setter, car le rôle de la matrice n'est pas d'empêcher un utilisateur d'écrire ou lire une valeur (elle s'en fiche, il n'y a aucun invariant), mais de construire une matrice d'une certaine taille. C'est comme std::vector, on peut accéder à n'importe quel élément via operator[], mais on ne peut pas modifier la partie qui s'occupe du stockage.

              Concernant get/set, il ne faut relativiser. Ce qui ne sert strictement à rien est une classe qui expose tous les membres avec un getter et un setter: cela ne sert à rien, il n'y a aucun invariant, autant mettre tout en public. L'utilisation d'un setter en lui-même est problématique dans le sens où il permet de modifier directement un membre. Le gros problème est qu'un setXXX() est un très mauvais nom, il ne représente pas un rôle, c'est une vision accès sur les données plutôt que les comportements, c'est à ce niveau que se situe le problème. L'utilisation de getter est du même acabit, mais cause moins de problème, lire un état est une chose bien plus fréquente est normal. Par exemple, la Matrice pourrait avoir 2 fonctions de lecture d'état: lignes() et colonnes(), car c'est une information importante pour l'utilisateur.

              ¹ Dans le jargon de C++, un allocateur est un concept existant: Allocator qui possède principalement 2 fonctions: allocate/deallocate (équivalent de new/delete), mais qui ne s'occupe pas de la manière d'accéder aux éléments.

              -
              Edité par jo_link_noir 23 novembre 2020 à 17:37:18

              • Partager sur Facebook
              • Partager sur Twitter
                23 novembre 2020 à 17:17:14

                En effet j'avais mal lu / lu trop vite ton précédent message ... 

                Concernant l'erreur en elle-même sur testSum, je ne la comprends pas, mais le typename T du template de la fonction est en conflit avec celui de la classe, il faut le renommer.

                Ok donc le T de mon template entre en conflit avec le T de ma classe, je modifie : 

                template<class U, class K>
                	friend Matrice<std::common_type_t < U, K>> testSum(Matrice<U> const& that, Matrice<K> const& other) {
                		assert((that.lines_ == other.lines_ && that.cols_ == other.cols_) && "Matrice should have the same size.");
                
                		Matrice<std::common_type_t < U, K>> ret{ that.lines_, that.cols_ };
                
                		for (std::size_t i = 0; i < that.lines_; i++)
                			for (std::size_t j = 0; j < that.cols_; j++)
                				ret.storage[i * ret.cols_ + j] = that.storage[i * ret.cols_ + j] + other.storage[i * ret.cols_ + j];
                
                		return ret;
                	}

                Erreur de compilation : 

                Erreur	C2995	'Matrice<common_type<U,K>::type> testSum(const Matrice<T> &,const Matrice<V> &)' : modèle de fonction déjà défini

                On dirait que même en changeant le typename T en autre chose, le compilateur "force" le premier paramètre de la fonction à être de type T

                À quoi servirait une classe Matrice si tu ne peux ni accéder aux éléments, ni les modifier

                Ben ... A fournir une visualisation d'une matrice, à sommer des matrices, les multiplier, les diagonaliser, trouver leur valeur propres, tester si elles sont triangulaires .... en 5 ans d'études scientifiques, je me suis jamais amusé à aller changer 1 coefficient dans ma matrice en pleins milieu d'un calcul. J'écris des matrices (constructeur), et je fais des calculs avec (les algos). Je ne vois absolument aucune raison d'autoriser l'utilisateur de ma classe à avoir une vue sur le contenue de mon Alocator<T> storage. Je n'ai besoin d'accéder aux éléments individuels de la matrice que pendant les algos de calculs matriciels, donc à l'intérieur de la classe Matrice. Jamais à l'extérieur.

                Je peux éventuellement mettre un getNbLignes() et un getNbCollones() oui un getSize() si l'utilisateur veut connaitre la taille de la matrice résultat de la multiplication de deux matrices de tailles différentes oui si on veut, mais à part ça ...

                > Tu ne vas quand même pas ajouter une fonction amie à chaque algorithme qui vont les manipuler ?

                Pourquoi pas ? Eclaire moi si je me trompe mais je ne vois absolument pas le soucis là dedans. La fonction amie me permet de garder privé mon Alocator<T> storage que l'utilisateur n'a pas à aller visiter, je donne l'accès au storage de façon contrôlé uniquement aux algos qui ont besoin d'aller fouiller dans storage et ça m'évite de faire des fonctions membres qui m'obligeraient à faire :

                Matrice<type> M1 {5,5,defaultVal}
                Matrice <otherType> M2 = {5,5,defaultValue}
                
                // Absolument pas intuitif, moche
                Matrice <type> M3 = M1.multiplication(M2);
                
                
                // Avec fonction amie
                Matrice <type> M3 = multiplication(M1,M2);
                
                Matrice <type> M4 = multiplication(Matrice<type>{5,5,def},Matrice<otherType>{5,5,def});

                Ca me semble tout même bien plus confortable d'utiliser des fonctions qui prennent en paramètre les deux matrices qu'on veut manipuler 

                Pour la fonction que m'avait donné lmghs, c'était uniquement pour répondre à ma question (débile) de "comment on fait". Je pensais déjà que c'était inutile de donner l'accès à l'utilisateur à un élément précis, je voulais juste essayer de le faire dans une démarche d'apprentissage et de manipulation des surcharges 

                • Partager sur Facebook
                • Partager sur Twitter
                  23 novembre 2020 à 17:44:32

                  Des exercices simples du coup (pas besoin de les coder, juste dire comment les faire):

                  • Comment créer une matrice 2x3 avec les valeurs 1,2,3,4,5,6 ?
                  • Comment l'écrire dans un fichier ?
                  • Comment l'afficher dans une UI Qt ?
                  • Comment laisser l’utilisateur implémenter ses propres algos ?

                  Concernant friend, je n'ai pas dit que les fonctions devraient être membres, je dis que friend ne devrait pas être nécessaire pour implémenter un algo. Devoir modifier le code pour ajouter un algo n'est pas une solution viable.

                  • Partager sur Facebook
                  • Partager sur Twitter
                    23 novembre 2020 à 20:50:47

                    - Comment créer une matrice 2x3 avec les valeurs 1,2,3,4,5,6

                    Un constructeur prenant 3 paramètres : nbLignes, nbColonnes, un containeur passé par ref et contenant les valeurs à copier dans storage

                    Matrice<int> M1{2,3,{1,2,3,4,5,6}};
                    

                    Ca ne me semble pas plus tordu comme ça plutôt que de faire

                    Matrice<int> M1{2,3};
                    M1(0,0) = 1;
                    ...
                    ...
                    ...
                    M1(2,3) = 6;

                    - Comment l'écrire dans un fichier ?

                    On peut imaginer 15 façon différentes de le faire avec une fonction membre. Pourquoi pas exactement de la même façon que je l'affiche dans la console ? Ligne par ligne avec une tabulation entre chaque valeur. On ajoute une ligne vide ou un caractère spécial derrière pour détecter la fin de la matrice à la lecture. Très facile de la reconstituer en lecture ligne par ligne. Ou on peut l'écrire dans le fichier texte comme ça [[1,2,3],[4,5,6]]. Là non c'est pas bien compliquer d'écrire une fonction membre qui fait ça.

                    - Qt ?

                    J'suis pas spécialement très calé en Qt mais encore une fois, on l'affiche de la même manière que je l'affiche en console dans un QTextEdit ou un truc du genre ?

                    - L'utilisateur écrit ses propres algos ?

                    Je met storage et mes deux attributs de taille en protected et ensuite aucun soucis pour écrire sa propre classe MatriceCustom qui hérite de Matrice, implémenter ses algos en interne dans une classe custom qui a accès au storage pour faire ce qu'on veut.

                    Je ne vois pas vraiment ce que tu essayes de prouver avec tes questions.

                    Tu sais, si j'insite avec ma proposition c'est pas par plaisir de te contredire. Je n'ai aucune envie de perdre du temps, et j'ai encore moins envie de faire perdre du temps aux gens qui veulent m'aider. Mais si tu t'attends à ce que je boive tes paroles comme un gourou je suis désolé mais je ne fonctionne pas comme ça.

                    Je cherche à réellement apprendre le C++, à construire des bases solides et à comprendre en profondeur ce que je fais. Pour ça j'ai besoin de comprendre pourquoi ce que j'essaye de faire est mauvais, et j'ai besoin de comprendre pourquoi ce que tu proposes marche et est meilleur. J'apprendrai jamais rien de durable si j'ai pas cette démarche.

                    Tu peux aller regarder mes récentes questions sur le forum, j'en ai posé pas mal. A chaque fois je discute oui, à chaque fois j'insiste sur mes propositions c'est vrai, mais c'est uniquement pour vous tirer les vers du nez jusqu'à ce que j'ai complètement compris pourquoi vos propositions sont bonnes et pas les miennes. Si tu regardes ces autres posts tu verras aussi que je suis de bonne foi, je fini toujours par me ranger à l'avis des seniors une fois que j'ai compris, mais pour ça faut m'expliquer, argumenter et me convaincre.

                    Là on a complètement dévié du sujet, tu essayes de me piquer sur une implémentation d'un accesseur mais franchement relis moi, j'ai quand même l'impression de pas juste être attentiste et d'espérer qu'on me donne tout dans le bec.

                    A chacune de tes réponses je reviens avec un code modifié, j'essaye des truc de mon côté, j'essaye de m'approprier le std::common_type que tu me donnes, j'essaye de comprendre dans quel cas ça marche et dans quel cas non ...

                    Là la conclusion que j'ai c'est que ta synthaxe marche en fonction libre mais pas en fonction amie.

                    > Concernant friend, je n'ai pas dit que les fonctions devraient être membres, je dis que friend ne devrait pas être nécessaire pour implémenter un algo. Devoir modifier le code pour ajouter un algo n'est pas une solution viable.

                    Ok, pas forcément membre, pas amie non plus. Quoi du coup ? C'est bien de dire ce qu'il faut pas faire mais je sais pas ce qu'il faut faire non plus. Il reste les fonctions libres ? J'ai argumenté mes réserves. J'ai peut-être tort je sais pas vu je ne reçois ensuite que des questions réthoriques.

                    Donc si t'as envie de m'aider, si tu acceptes de prendre un peu de temps pour partager ton expérience avec une personne motivée pour réellement apprendre, casse mes arguments, étaye les tiens, montre moi des morceaux de code. Et je t'en remercierai énormément

                    Si t'as juste envie d'essayer de me casser ou que je t'écoute aveuglement sans comprendre ni apprendre ce n'est par contre pas ce que je recherche.

                    Cordialement

                    • Partager sur Facebook
                    • Partager sur Twitter
                      24 novembre 2020 à 0:03:00

                      Il ne faut pas prendre la mouche, les exercices de penser de mon dernier post était pour de faire réfléchir sur un point: comment peut-on utiliser la classe Matrice ? Problème, tu ne vois pas l'absurde de tes réponses :/

                      Pour le premier, les 2 solutions ne sont pas incompatibles (m(x,y) et ctor), mais les 3 autres sont beaucoup plus intéressants et révélateur d'un problème: la classe matrice n'est absolument pas réutilisable.

                      > - Comment l'écrire dans un fichier ? > On peut imaginer 15 façons différentes de le faire avec une fonction membre. [...] > - Comment l'afficher avec Qt ? > encore une fois, on l'affiche de la même manière [...]

                      Tu proposes d'ajouter dans la matrice des fonctions qui n'ont absolument rien à voir avec le but premier de la classe Matrice. Dès qu'un besoin apparaît pour faire quelque chose des valeurs, hop, on l'ajoute à Matrice. Maintenant, pour faire un bête calcul, on se chope une dépendance à Qt. Si on veut un affichage un peu différent dans un autre contexte, hop pareil. Sérialiser en json ? idem nouvelle dépendance, nouvelle fonction. Et quand on veut réutiliser la classe pour un autre projet c'est l'enfer, il y a des centaines de fonctions et de dépendances qui n'ont rien à faire là. Mais à quoi sert la Matrice ? Ah oui, du calcul... Pourtant, le moindre petit besoin oblige de la modifier et de lui ajouter des comportements tout autre.

                      Toutes ces fonctions ont une chose en commun: elles ont besoin de connaître les valeurs contenues dans la matrice. Sans un moyen pour y parvenir, elles ne peuvent rien faire. D'ailleurs que feras-tu avec après application des algos ? À un moment il faudra bien lire ce qui a dedans.

                      Le principe même d'une classe est de fournir un service (pour Matrice, faire du calcul matriciel) qu'on pourra réutiliser dans d'autres à travers une bibliothèque. Si on veut écrire dans un fichier, ce n'est pas à cette classe de le faire, ce n'est pas son but, cette fonction ne doit pas être dans cette classe. Pourtant, écrire une matrice dans un fichier est très utile et c'est quelque chose qu'on voudrait pouvoir faire sans modifier la bibliothèque. S'il n'est pas possible de lire les valeurs sans modifier la bibliothèque, cette bibliothèque n'est simplement pas (ré)utilisable.

                      > - Comment l'utilisateur écrit ses propres algos ? > Je mets storage et mes deux attributs de taille en protected et ensuite aucun souci pour écrire sa propre classe MatriceCustom qui hérite de Matrice

                      Donc pour étendre la classe, il faut faire sauter les invariants et finalement rendre accessible tous les membres. Ce n'est pas loin d'avoir tout en public finalement. Un algo est une matrice ou c'est l'inverse ? :D

                      Du coup, pour l'algo A, je vais faire un nouveau type MatriceA qui implémente un seul et unique algo (pas super pratique). Puis pareil pour l'algo B. Et maintenant j'ai un autre algo (C), qui dépend des 2 premiers et qui va lui-même modifier certaines valeurs directement dans la matrice. Mmmh problème, comment je modifie les valeurs de MatriceA avant d'appliquer B ? Ah, mais MatriceA n'est pas une MatriceB, comment j'applique l'algo B ?

                      Le but est de manipule un type Matrice, à quoi servent finalement toutes ses classes qui ne sont ni plus ni moins que des matrices ? Pourquoi je pourrais faire m*=2, mais pas labToRgb(m) sans passer par des intermédiaires farfelus ? Car finalement, une multiplication et une transformation de matrice c'est la même chose: un algo. Quand on fait un calcul sur une matrice, c'est normal de vouloir la modifier.

                      Ce qui te manque surtout est un cas d'utilisation. Tu te rendras vite compte qu'en absence de fonctions de lecture et écrire, il est impossible d'implémenter la moindre chose sans continuellement modifier la classe. Alors que c'est juste un conteneur qui ajoute des simplifications pour le calcul matriciel. Image ce que serait std::vector sans operator[].

                      -
                      Edité par jo_link_noir 24 novembre 2020 à 0:09:14

                      • Partager sur Facebook
                      • Partager sur Twitter
                        24 novembre 2020 à 5:10:46

                        Salut,

                        Pour t'aider à comprendre le dernier message de jo_link_noir...

                        Il existe un principe essentiel en programmation (en fait, il y en a cinq, qui sont connus sous l'acronyme SOLID, on va surtout s'intéresser au S ;) ). Il s'appelle le principe de la responsabilité unique (Single Responsability Principle ou SRP, en anglais).

                        Ce principe est à la fois très simple à comprendre et très compliqué à appliquer correctement, car il nous dit en substance que

                        chaque chose -- que ce soit une fonction, un type de donnée ou une donnée -- ne doit servir qu'à un seul et unique but, pour que l'on puisse garantir qu'elle s'en occupe correctement

                        C'est simple à comprendre, non?

                        La première question que tu dois te poser est donc

                        A quoi va me servir ma classe Matrice

                        ou, si tu préfères

                        Si je reçois (de la part de n'importe qui) une classe Matrice<T>, quelles sont les choses que je m'attends à pouvoir faire et qui n'impliquent que la classe Matrice<T>

                        Voyons un peu voir les possibilités qui nous sont offertes :

                        • L'enregistrer dans un fichier donné à un format quelconque? non, cela implique un fichier et un format
                        • l'afficher dans une interface graphique utilisateur (IHM)? non, cela implique une IHM
                        • la multiplier ou l'additionner avec une matrice contenant des données de types différents?non, car cela implique une matrice<T> et une Matrice<U>. Ce sont donc deux classes différentes
                        • Connaitre le nombre de lignes et de colonnes utilisées par la matrice ? ah, oui, car, ca n'implique que la matrice, et c'est vachement utile :D
                        • Copier les données d'une matrice pour en obtenir une autre identique (constructeur de copie)? assigner les valeurs d'une matrice existantes à une autre matrice existante (opérateur =) ? ah, oui, car, ça n'implique que la matrice, et c'est vachement utile :D
                        • Accéder (en lecture seule) à la donnée qui se trouve à la ligne l et à la colonne c? ah, oui, car, ça n'implique que la matrice, et c'est vachement utile :D
                        • modifier la valeur de la donnée qui se trouve à la ligne l et à la colonne c? ah, oui, car, ça n'implique que la matrice, et c'est vachement utile :D
                        • Modifier les valeurs de toutes les données par addition d'une autre matrice (contenant le même type de donnée)? éventuellement, si cela a du sens
                        • Modifier la matrice en la transposant? Hummm... ca risque de poser problème, car il faut savoir si on a la matrice "d'origine" ou la matrice transposer (et la re transposer le cas échéant)... non

                        A partir de là, tu ne gardes que les cas où la réponse a été un "oui" franc, et tu te retrouves donc avec une matrice qui devrait ressembler à

                        template <typename T>
                        class Matrice{
                        public:
                            /* je n'implémente rien, c'est juste pour montrer
                             * l'interface qu'elle doit avoir
                             */
                            Matrice(size_t row, size_t column,T defaultValue= t{});
                            Matrice(Matrice<T> const &);
                            Matrice<T>& operator =(Matrice<T> const &);
                            size_t rows() const;
                            size_t columns() const;
                            T operator() (size_t row, size_t columns) const;
                            T& operator() (size_t row, size_t columns);
                            /* cet opérateur particulier est déjà limite ;-) */
                            Matrice & operator +=(Matrice<T> const &);
                        };

                        Tout le reste, ca n'a rien à faire au niveau de ta classe Matrice en elle-même...

                        Cependant, comme tu peux en avoir besoin, il faudra soit fournir des fonctions libres, soit fournir des abstractions qui te permettent d'obtenir le résultat souhaité, par exemple

                        template <typename T>
                        void print(std::ostream& out, Matrice<T> const & m);
                        template <typename T, typename U>
                        Matrice<std::common_type<T,U>::type> operator +(Matrice<T> const & a, Matrice<U> const & b);
                        template <typename T>
                        Matrice<T> transpose(Matrice<T> const & m);
                        /* ne manque que l'exemple de l'IHM, qui passera idéalement par la création d'un widget spécifique */ 

                        De cette manière, tu n'ajoute aucune dépendance "indue" (même pas une dépendance avec les flux de sortie de la bibliothèque standard) à ta classe Matrice, car, si tu as besoin d'une de ces fonctions (ou d'une de ces abstractions) libres, ben, tu peux en définir le comportement "là où tu en as besoin".

                        Il n'y a, au final, que l'addition de deux matrice contenant des données de types différents qui pourrait -- éventuellement -- se retrouver dans le même fichier que l'implémentation de la classe Matrice proprement dite ;)

                        S'il y a bien un principe que tu dois absolument respecter, c'est, clairement, le SRP.

                        A l'exception de l'OCP (qui vient ex-aequo avec le SRP en termes de nécessité de le respecter), le respect detous les autres principes SOLID découlera presque automatiquement du respect de ces deux principes de base ;)

                        Et donc, pour faire simple : quand quelqu'un te lance une perche "empoisonnée" en te demandant comment tu compte t'y prendre pour arriver à faire quoi que ce soit avec une instance de ta classe, si ca ne fait pas partie des quelques services "de base" rendus par ta classe, c'est qu'il n'y a aucune raison de rajouter la possibilité au sein même de ta classe :D

                        • 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 novembre 2020 à 12:40:50

                          Bonjour, je n'ai pas eu le temps de revenir vers vous avant mais merci pour vos réponses. Je pense que c'est plus clair de mon esprit et je comprend pourquoi ce que faisais au début était trop fermé pour l'utilisateur

                          J'ai donc réduit le contenue de ma classe aux différents constucteur, une fonction swap et les opérateurs () , += , -= , *= qui ont du sens

                          // Classe Matrice suivant le principe RAII
                          template<typename T>
                          class Matrice
                          {
                          public:
                          
                          	Matrice(std::size_t ligne, std::size_t col)
                          		: lines_(ligne)
                          		, cols_(col)
                          		, storage(ligne* col, {})
                          	{	}
                          
                          
                          	Matrice(std::size_t ligne, std::size_t col, T def)
                          		: lines_(ligne)
                          		, cols_(col)
                          		, storage(ligne* col, def)
                          	{	}
                          
                          	Matrice(Matrice const& mat) = default;
                          	Matrice(Matrice&& mat) = default;
                          	~Matrice() = default;
                          
                          	friend void swap(Matrice& that, Matrice& mat) noexcept {
                          		using std::swap;
                          
                          		swap(that.lines_, mat.lines_);
                          		swap(that.cols_, mat.cols_);
                          		swap(that.storage, mat.storage);
                          	}
                          
                          	std::size_t lines() const noexcept { return lines_; };
                          	std::size_t columns() const noexcept { return cols_; };
                          
                          	// -------- Operator overloading --------
                          
                          	void operator=(Matrice mat) noexcept {
                          		swap(*this, mat);
                          	}
                          
                          	T& operator()(std::size_t i, std::size_t j) noexcept {
                          		assert(i < lines_ && "Index out of range");
                          		assert(j < cols_ && "Index out of range");
                          		return storage[i * cols_ + j];
                          	}
                          
                          	T const& operator()(std::size_t i, std::size_t j) const noexcept {
                          		assert(i < lines_ && "Index out of range");
                          		assert(j < cols_ && "Index out of range");
                          		return storage[i * cols_ + j];
                          	}
                          
                          	Matrice& operator+=(Matrice<T> const& mat) {
                          		assert((this->lines_ == mat.lines_ && this->cols_ == mat.cols_) && "Matrice should have the same size.");
                          
                          		for (std::size_t i = 0; i < lines_; i++)
                          			for (std::size_t j = 0; j < cols_; j++)
                          				storage[index(i, j)] += mat(i, j);
                          
                          		return *this;
                          	}
                          
                          	Matrice& operator-=(Matrice<T> const& mat) {
                          		assert((this->lines_ == mat.lines_ && this->cols_ == mat.cols_) && "Matrice should have the same size.");
                          		for (std::size_t i = 0; i < lines_; i++)
                          			for (std::size_t j = 0; j < cols_; j++)
                          				storage[index(i,j)] -= mat(i, j);
                          
                          		return *this;
                          	}
                          	
                          	// Je ne peux pas travailler directement sur *this et faire return *this car le résultat n'est pas forcément une matrice de même taille que this
                          	// Je dois passer par une matrice temporaire et utiliser l'opérateur d'affectation pour correctement modifier *this
                          	void operator*=(Matrice<T> const& mat) {
                          		assert((this->lines_ == mat.cols_ && this->cols_ == mat.lines_) && "Incorrect matrix size for multiplication.");
                          
                          		Matrice<T> temp{ this->lines(), mat.columns() };
                          		
                          		for (std::size_t i = 0; i < lines_; i++)
                          			for (std::size_t j = 0; j < lines_; j++)
                          				for (std::size_t k = 0; k < cols_; k++)
                          					temp(i,j) += storage[index(i,k)] * mat(k, j);
                          			
                          		*this = temp;
                          	}	
                          	
                          
                          	/*  Scalar multiplication  */
                          	Matrice& operator*=(T t) {
                          		for (std::size_t i = 0 ; i < lines_; i++)
                          			for (std::size_t j = 0; j < cols_; j++)
                          				storage[index(i, j)] *= t;
                          
                          		return *this;
                          	}
                          
                          	
                          	// --------------------------------------
                          protected:
                          	std::size_t index(std::size_t i, std::size_t j) { return (i * cols_ + j); }
                          
                          	std::size_t lines_;
                          	std::size_t cols_;
                          
                          	Alocator<T> storage;
                          
                          };

                          Et ensuite seulement, je définie en fonction libre dans le même header (comme ça ces fonctions libre sont inclues en même temps que ma classe quand je fais #include "Matrice.h") d'autres opérateurs et algo qui me permettent de travailler avec deux matrices de type différent par exemple : 

                          template<typename T>
                          std::ostream& operator<<(std::ostream& os, Matrice<T> const& mat) {
                          }
                          
                          template<typename T, typename K>
                          Matrice<std::common_type_t < T, K>> operator+(Matrice<T> const& mat, Matrice<K> const& other) {
                          	assert((mat.lines() == other.lines() && mat.columns() == other.columns()) && "Matrice should have the same size.");
                          
                          	Matrice<std::common_type_t < T, K>> ret{ mat.lines(), mat.columns() };
                          
                          	for (std::size_t i = 0; i < mat.lines(); i++)
                          		for (std::size_t j = 0; j < mat.columns(); j++)
                          			ret(i, j) = mat(i, j) + other(i, j);
                          
                          	return ret;
                          }
                          
                          template<typename T, typename K>
                          Matrice<std::common_type_t < T, K>> operator*(Matrice<T> const& mat, K k) {
                          }
                          
                          
                          template<typename T, typename K>
                          Matrice<std::common_type_t < T, K>> operator*(Matrice<T> const& mat, Matrice<K> const& other) {
                          }
                          
                          
                          template<typename T, typename K>
                          Matrice<std::common_type_t < T, K>> operator-(Matrice<T> const& mat, Matrice<K> const& other) {
                          }
                          
                          // Eventuellement plus d'opération / algo prédéfinis

                          Est ce que cette fois cette structuration du code est correcte ? Ca me semble respecter le SRP ainsi que l'OCP


                          • Partager sur Facebook
                          • Partager sur Twitter
                            28 novembre 2020 à 21:18:45

                            Bonjour,

                            La structuration est plutôt bonne mais tu es en train de recréer 2 fois les même codes. Ton operator+= n'est qu'un cas particulier de ta fonction libre elle devrait l'utiliser pour ne pas écrire 2 fois des codes quasi identiques.

                            Matrice& operator+=(Matrice const& mat) {
                                return *this = *this + mat;     // utilise l'operator libre (comme *this et mat ont le même type, le type du résultat est bien celui de *this)
                            }

                            Et tu as oublié de retourner ton résultat pour l'operator*=.

                            • Partager sur Facebook
                            • Partager sur Twitter

                            En recherche d'emploi.

                              29 novembre 2020 à 0:49:02

                              Dalfab a écrit:

                              Bonjour,

                              La structuration est plutôt bonne mais tu es en train de recréer 2 fois les même codes. Ton operator+= n'est qu'un cas particulier de ta fonction libre elle devrait l'utiliser pour ne pas écrire 2 fois des codes quasi identiques.

                              Matrice& operator+=(Matrice const& mat) {
                                  return *this = *this + mat;     // utilise l'operator libre (comme *this et mat ont le même type, le type du résultat est bien celui de *this)
                              }

                              Et tu as oublié de retourner ton résultat pour l'operator*=.

                              En fait, c'est plutôt l'inverse: c'est l'opérateur libre +  qui devrait n'être qu'un cas particulier de l'opérateur +=, sous une forme proche de

                              template <typename T>
                              class Matrix{
                              public:
                                  Matrix & operator +=(Matrix<T< const &){
                                      /* je passe la logique */
                                      return *this;
                                  }
                              };
                              template <typename T>
                              Matrix<T> operator + (Matrix<T> const & a, Matrix<T> const & b){
                                  Matrix<T> copy{a};
                                  return copy+=b;
                              }
                              • 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
                                29 novembre 2020 à 2:40:42

                                Petit note concernant return copy+=b;: c'est à éviter. Cela empêche la NRVO, car += retourne une référence. C'est préférable de le faire en 2 lignes.

                                • Partager sur Facebook
                                • Partager sur Twitter
                                  30 novembre 2020 à 9:00:35

                                  Vous avez raison, je n'avais pas fait attention au fait que j'avais une duplication de code.

                                  Dans mon cas la proposition de daflab fonctionne tout à fait puisque comme il l'a dit, on sait que *this et mat sont de même type donc pas de soucis pour écraser la valeur de *this par la somme des deux matrices

                                  En revanche il n'est pas possible, avec ma structure de code actuelle, d'utiliser l'opérateur += pour définir la fonction libre qui surcharge l'opérateur +. 

                                  Le but de cet opérateur + en fonction libre est de pouvoir sommer deux matrices de types différent lorsque ça a du sens par exemple 

                                  int main()
                                  {
                                      Matrice<int> A{ 3, 3, 1 };
                                      Matrice<double> B{ 3, 3, 1 };
                                      auto C = A + B;  // int + double à du sens
                                      std::cout << C << std::endl;
                                  
                                      A += B; // Ne fonctionne pas car A et B sont de type différent
                                  
                                      return 0;    
                                  }

                                  Mais il est vrai que comme Koala, j'ai plus l'habitude d'utiliser += pour définir + que l'inverse.

                                  Est ce que ça résulte d'une mauvaise structure du code ou est-ce que je suis dans un cas particulier où c'est nécéssaire / pertinent d'utiliser l'opérateur + pour définir l'opérateur += ?

                                  Pour appliquer la proposition de Koala, il faudrait que la surcharge de l'opérateur += soit aussi une fonction libre qui change le type de l'opérande de gauche et qui permettrait d'écrire :

                                  int main()
                                  {
                                      Matrice<int> A { 3, 3, 1 };
                                      Matrice<double> B { 3, 3, 1 };
                                  
                                      A += B; // Conversion implicite de A de Matrice<int> en Matrice<double>
                                      std::cout << A << std::endl;
                                  
                                      return 0;    
                                  }

                                  Je ne sais même pas si c'est possiblede faire ça, avoir l'opérateur += qui change le type de l'opérande de gauche mais même si c'est possible j'ai pas l'impression que ça soit une bonne chose à faire ...

                                  Qu'est ce que vous en pensez ?



                                  -
                                  Edité par ThibaultVnt 30 novembre 2020 à 9:02:15

                                  • Partager sur Facebook
                                  • Partager sur Twitter
                                    30 novembre 2020 à 23:55:36

                                    Le fait est que, si tu y regardes bien, il est simplement "plus logique" d'utiliser l'opérateur += pour définir l'opérateur + que l'inverse.

                                    Car, voici à quoi ressemble la première solution:

                                    template <typename T>
                                    class Matrix{
                                    
                                    Matrix<T> & operator +=(Matrix<T> const & other){
                                        assert(other.rows() == rows_ && "inconsistant row count");
                                        assert(other.columns()== cols_ && "inconsistant column count");
                                        /* ce dernier assert ne devrait pas être nécessaire */
                                        assert(rows_ * cols_ == datas_.size() && "inconsistant item count");
                                        for(size_t r{0};r <rows_;++r){
                                            for(size_t c{0}; c <cols_; ++c){
                                                datas_[r*rows_ + c]= other(r,c);
                                            }
                                        }
                                        return *this;
                                    }
                                    private:
                                        size_t rows_;
                                        size_t cols_;
                                        std::vector<T> datas_;
                                    };
                                    template <typename T>
                                    Matrix<T> operator+(Matrix<T> const & a, Matrix<T> const & b){
                                         Matrix<T> copy{a}; // copie une des matrice
                                         copy += b; // modifie la copie
                                         return copy; // renvoie la copie
                                    }

                                    Nous avons donc un opérateur += qui ne fait pas de copie mais qui modifie directement les données dont il dispose et un opérateur + qui ne fait une copie que parce qu'il y est contraint et forcé (du fait que les deux matrices originales sont constantes), ce qui ne pose pas de problème car la durée de vie de la copie sera étendu à la durée de vie de la variable qui récupérera le résultat dans la fonction appelante.

                                    Et voici à quoi ressemblerait une situation utilisant l'opérateur + pour définir l'opérateur +=:

                                    /* on n'a pas vraiment le choix quant à la syntaxe de l'opérateur +:
                                     * les deux paramètres doivent être des références constantes
                                     * et le résultat est renvoyé sous forme de valeur
                                     */
                                    template <typename T>
                                    Matrix<T> operator + (Matrix<T> const & a, Matrix<T> const & b){
                                        assert(a.rows()==b.rows() && "inconsistant row count");
                                        assert(a.columns() == b.columns() && "inconsitant column count");
                                        /* on n'a donc pas le choix: il faut copier la première
                                         * matrice
                                         */
                                        Matrix<T> copy{a};
                                        for(size_t r{0};r<a.rows();++r){
                                            for(size_t c{}; c<a.columns(); ++c){
                                                /* on modifie la copie */
                                                copy(r,c) += b(r,c);
                                            }
                                        }
                                        /* avant de renvoyer la copie */
                                        return copy
                                    }
                                    template <typename T>
                                    class Matrix{
                                    
                                    Matrix<T> & operator +=(Matrix<T> const & other){
                                        *this = *this + other;
                                        return *this;
                                    }
                                    private:
                                        size_t rows_;
                                        size_t cols_;
                                        std::vector<T> datas_;
                                    };
                                    

                                    En termes de lignes, nous sommes sensiblement au même point (surtout si on retire les commentaires que j'ai ajoutés :D ), donc, on pourrait se demander ce qui change dans l'histoire, juste?

                                    De même, le fonctionnement de l'opérateur + ne change, basiquement absolument pas:

                                    • on crée une copie (parce que l'on n'a pas le choix),
                                    • on modifie la copie
                                    • on renvoie la copie par valeur.

                                    Là où cela se corse, c'est au niveau de l'opérateur +=, car, la première lignes va avoir une effet pour le moins étrange vu

                                    • Qu'elle fait explicitement appel à l'opérateur +, et donc, obligatoirement provoquer une copie de *this (et la modification de cette copie)
                                    • qu'elle fait explicitement appel à l'opérateur = qui va --typiquement
                                      • faire une copie de la copie modifiée de *this (passée par référence constante, rappelons le)
                                      • intervertir les valeurs rows_, cols_ et datas_ entre *this et la copie de la copie modifiée de this
                                      • détruire la copie de la copie modifiée de this, vu que l'on quitte la portée de l'opérateur = et que la copie de la copie modifiée de this n'est pas renvoyée par l'opérateur =
                                    • détruire la copie modifiée de this au plus tard à l'accolade fermante de l'opérateur += (en fait, ce serait même au niveau de l'accolade fermante de l'opérateur =, vu qu'il s'agit en l'occurrence d'une variable temporaire anonyme), vu qu'elle ne sert plus à rien

                                    Du coup, si l'opérateur + est "aussi efficace que possible", l'opérateur += nous oblige à payer le prix de deux copies totalement inutiles dans le cadre de son utilisation et contre lesquels aucun mécanisme n'est capable de nous immuniser.

                                    Bon, ce n'est -- a vrai dire -- pas tout à fait vrai, car, à l'extrême limite, le fait que l'opérateur += utilise en réalité une variable temporaire anonyme lors de l'appel à l'opérateur = devrait pouvoir nous permettre d'utiliser l'opérateur = par déplacement, ce qui devrait nous permettre d'économiser une des deux copies.

                                    Par contre, il n'y a aucun moyen d'éviter la création de la copie destinée à être modifée. Et une copie, ca reste toujours plus que 0 copie :D

                                    • 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
                                      1 décembre 2020 à 9:08:52

                                      Merci pour ta réponse Koala.

                                      J'étais arrivé aux même codes que ce que tu montre. Celui qui utilise l'opérateur + pour définir += et celui qui utilise += pour définir +. Je m'étais aussi rendu compte que ça m'obligeais à effectuer des copies de matrice dans += que je n'avais pas sinon. Bien sur, je ne l'avais pas compris avec autant de détail que ce que tu m'as donné ;)

                                      Cependant, comme je l'ai dit dans mon précédent message, l'opérateur += n'a de sens que sur des matrices de même type. Utiliser l'opérateur += pour définir l'opérateur + nous permet certes d'éviter la duplication de code et les copies inutiles dans += mais ça a pour conséquence d'interdire la somme de Matrice de type différent. Le prototype de la fonction operator+ que tu me proposes le confirme, le type de retour et celui des deux paramètres sont identiques.

                                      Je trouve tout de même que ça a beaucoup de sens de sommer une Matrice d'entier avec une Matrice de double. C'est un comportement que j'aimerais conserver dans la mesure du possible mais je ne vois pas comment faire sans dubliquer bêtement le code de la somme dans chaque opérateur. Là au moins, l'opérateur += travail directement sur une référence et n'effectue aucune copie et je conserve le comportement général sur l'opérateur + qui me permet de sommer deux matrices de type différent.

                                      Je ne suis pas sûr qu'il y ai une solution qui me permettrait d'avoir le beurre, l'argent du beurre et le c....

                                      • Partager sur Facebook
                                      • Partager sur Twitter
                                        1 décembre 2020 à 14:40:06

                                        ThibaultVnt a écrit:

                                        Cependant, comme je l'ai dit dans mon précédent message, l'opérateur += n'a de sens que sur des matrices de même type.

                                        Mais ca, on s'en fout, vu que l'addition de deux matrices contenant des types différents n'est pas du ressort de la matrice en elle-même (relis ma première intervention à ce sujet :D ) et que tu devras donc te "limiter" à la définition d'une fonction libre prenant la forme de

                                        template <typename T, typename U>
                                        Matrix<R> operator +(Matrix<T> const & a, Matrix<U> const & b){
                                            /* la logique sera indiquée plus tard 
                                        }

                                        ThibaultVnt a écrit:

                                        Le prototype de la fonction operator+ que tu me proposes le confirme, le type de retour et celui des deux paramètres sont identiques.

                                        Oui, parce que c'est le cas le plus simple et qui  suffisait à démontrer ce que je tentais d'expliquer ;)

                                        ThibaultVnt a écrit:

                                        mais ça a pour conséquence d'interdire la somme de Matrice de type différent.

                                        Non, la seule conséquence que l'utilisation de l'opérateur += pour définir l'opérateur + est de te forcer à faire "quelque chose de plus" si tu souhaite permettre l'addition de matrices contenant des données de types différentes...

                                        D'ailleurs, je ne sais pas si tu l'as remarqué, mais la matrice renvoyée par l'opérateur tel que je viens de te le montrer pour deux types de données différents est sensée contenir des données dont le type est ce qui est commun entre T et U, parce que, en réalité, le prototype complet de cet opérateur serait en fait proche de

                                        /* cf la première réponse de jo_link_noir */
                                        template <typename T, typename U, typename R = std::common_type<T, U>::type>
                                        Matrix<R> operator + (Matrix<T> const &a, Matrix<U> const & b){
                                            /* la logique suivra */
                                        }

                                        Or, ce qui est commun entre T et U pour, pour autant que l'on sache, 

                                        • correspondre au type modélisé par le type T
                                        • correspondre au type modélisé par le type U
                                        • ou même correspondre à n'importe quel type différent de T et de U qui soit pourtant commun aux deux

                                        Si bien que, pour arriver à modéliser le comportement de cet opérateur, il nous est totalement impossible de déterminer si le type commun sera fourni par l'opérande de gauche (Matrix<T>), par l'opérande de doite (Matrix<U>) ou ... par "autre chose" :p

                                        Pour contourner cette impossibilité, nous n'avons donc qu'une seule solution plausible: nous assurer de créer deux matrices contenant des données de type R (ou R est le type commun entre T et U) de telle manière à ce que la première contienne les données originaire de l'opérande de gauche et que la deuxième  contienne les données originaires de l'opérande de droite.

                                        Et, une fois que ce sera fait, tu pourras sans problème ... utiliser l'opérateur membre += (de ta Matrix<R>) pour effectuer ton calcul :D

                                        "Tout ce qui peut te manquer", dans le pire des cas, c'est le moyen de créer une Matrix<R> à partir d'une Matrix<X> ;)

                                        Mais, ca, ca peut aussi "assez facilement" se résoudre à l'aide d'une fonction libre qui prendrait une forme proche de

                                        template <typename R, typename T>
                                        Matrix<R> convert(Matrix<T> const & a){
                                            Matrix<R> result{a.rows(), a.cols()}; 
                                            for(size_t r{0};r<a.rows();++r){
                                                for(size_t c{0};c<a.cols();++c){
                                                    result(r,c)=static_cast<R>(a(r,c));
                                                }
                                            }
                                            return result;
                                        }

                                        Et, grâce à cette fonction libre, nous pourrons donc fournir une implémentation "finale" pour notre opérateur + afin de permettre l'addition de deux matrices manipulant des données de types différents sous une forme proche de

                                        template <typename T, typename U, typename R = std::common_type<T, U>::type>
                                        Matrix<R> operator + (Matrix<T> const &a, Matrix<U> const & b){
                                           auto aCopy = convert<R>(a);
                                           auto bCopy = convert<R>(b);
                                           aCopy  += bCopy;
                                           return aCopy;
                                        }

                                        Et, le mieux de tout, c'est que le compilateur choisira tout seul la version "la plus efficace" de notre opérateur +:

                                        Il choisira la version ne nécessitant qu'un seul paramètre template si on additionne deux matrices contenant des int (ou deux matrices contenant des double, ou n'importe quoi d'autre), et la version nécessitant deux (voir trois) paramètres template uniquement si, de fait, nous essayons d'additionner deux matrices contenant des données de type différents.

                                        Magique, non ??? :D

                                        -
                                        Edité par koala01 1 décembre 2020 à 14:41:01

                                        • 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
                                          1 décembre 2020 à 16:00:34

                                          Ouiiiiii ok j'ai compris !! [GIF du mec qui a le cerveau qui explose sous le coup d'une grande révélation]

                                          En fait j'avais fait que les 2/3 du boulot.

                                          J'avais l'opérateur += pour deux Matrice de même type et j'avais un opérateur + qui était au final trop général pour être efficace en faisant le travail de deux fonctions normalement distinctes 

                                          Je pense que là j'ai bien compris, merci beaucoup.

                                          Juste une dernière questions mais qui relève vraiment du détail. Dans l'opérateur + qui somme deux matrices de même type, on a déjà dit qu'on avait pas le choix et qu'il fallait faire une copie. Dans ce cas, est-ce qu'il y a une différence entre ces deux propositions ?

                                          // Proposition 1, ref constante de l'opérande de gauche
                                          template <typename T>
                                          Matrix<T> operator+(Matrix<T> const & a, Matrix<T> const & b){
                                               Matrix<T> copy{a}; // On fait une copie
                                               copy += b;
                                               return copy;
                                          }
                                          
                                          
                                          // Proposition 2 : Operande de gauche passé par valeur
                                          template <typename T>
                                          Matrix<T> operator+(Matrix<T> a, Matrix<T> const & b){
                                               // Pas de besoin de manuellement copier la matrice, a est déjà une copie de la matrice créée dans le main
                                          
                                               a += b;
                                               return a;
                                          }

                                          J'ai l'impression que cela revient au même en espace mémoire utilisé ainsi qu'en temps d'éxécution. Est ce qu'il y a encore un obscure concept qui se cache là dessous et qui différencie ces deux approches et devrait me faire privilégier l'un ou l'autre ou sont-elles bel et bien identiques ?

                                          -
                                          Edité par ThibaultVnt 1 décembre 2020 à 16:01:26

                                          • Partager sur Facebook
                                          • Partager sur Twitter
                                            1 décembre 2020 à 19:42:52

                                            A priori, les deux solutions seront aussi performantes l'une que l'autre.  Et il n'est même pas impossible que la deuxième version "économise" l'utilisation de l'équivalent d'un pointeur (8 bytes sur les systèmes 64 bits) lors de l'appel de la fonction :D

                                            Cependant, j'ai quand même tendance à préférer la première solution parce que

                                            • "par habitude", je transmet "tout ce qui est plus gros qu'un pointeur" par référence (constante si la fonction ne doit pas le modifier): la première solution m'évite donc de briser cette habitude
                                            • et (surtout :D ) la première solution rend la copie de l'élément transmis comme paramètre explicite ce qui évite au lecteur du code d'avoir à suivre un raisonnement du genre de "ah oui, le passage par valeur occasionne la copie --> la fonction modifie donc une copie de l'opérande de gauche de ma formule"

                                            J'ai en effet appris depuis longtemps que, plus les choses sont explicites, moins on risque d'avoir de problème de compréhension à la lecture :D

                                            • 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
                                              1 décembre 2020 à 20:32:14

                                              Oui ok, je me suis douté que c'était pour une question d'habitude / continuité de tout ce qu'on a déjà écrit mais je voulais être sûr

                                              Merci pour les explications en tout cas, j'ai largement tout ce qu'il me fallait je passe le sujet en résolu

                                              • Partager sur Facebook
                                              • Partager sur Twitter

                                              Somme de matrice template

                                              × 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