FXNLIbKnEeitmQqVuF7yZg Corriges-Init-W05
FXNLIbKnEeitmQqVuF7yZg Corriges-Init-W05
FXNLIbKnEeitmQqVuF7yZg Corriges-Init-W05
Corrigés semaine 5
Les corrigés proposés correspondent à l'ordre des apprentissages : chaque corrigé correspond à la solution à laquelle vous
pourriez aboutir au moyen des connaissances acquises jusqu'à la semaine correspondante.
A)
Le code fourni remplit le vecteur tab (de taille 10) d'éléments allant de 0 à 9.
En effet, push_back ajoute un élément à la fin du tableau. Au moment de l'ajout tab.size() vaut la taille du vecteur
avant l'ajout (puisque l'élément n'est pas encore ajouté).
affiche(nt) :
0
1
2
3
4
5
6
7
8
9
B)
Ajoute à la fin de tab2 un vecteur de même taille que le vecteur tab et contenant que des éléments de même valeur : la
valeur du premier élément de tab.
Dans le cas où tab2 est un tableau vide (et dans ce cas seulement), on pourrait aussi faire :
Nous savons depuis la leçon sur les fonctions qu'il ne faut jamais faire de copier-coller dans du code, mais faire des
fonctions ! C'est par exemple le cas ici pour la saisie des vecteurs (cet aspect est optionnel, mais c'est bien d'y penser !).
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
cout << "Quelle taille pour les vecteurs ? ";
size_t n;
cin >> n;
vector<double> v1(n);
vector<double> v2(n);
saisie("premier", v1);
saisie("second", v2);
return 0;
}
// --------------------------------------------------------
double scalaire(vector<double> u, vector<double> v)
{
double somme(0.0);
return somme;
}
// --------------------------------------------------------
void saisie(const string& titre, vector<double>& vecteur)
{
cout << "Saisie du " << titre << " vecteur :" << endl;
for (size_t i(0); i < vecteur.size(); ++i) {
cout << " coordonnée " << i+1 << " = ";
cin >> vecteur[i];
}
}
Exercice 17 : multiplication de matrices
Le code fourni ici est en C++11. Pour une version compilant avec l'ancien standard (C++98) voir ci-dessous . Voir aussi les
commentaires en fin de corrigé.
#include <iostream>
#include <vector>
using namespace std;
vector<vector<double>> lire_matrice();
void affiche_matrice(const vector<vector<double>>& M);
vector<vector<double>> multiplication(const vector<vector<double>>& M1,
const vector<vector<double>>& M2);
// --------------------------------------------------------------------
int main()
{
vector<vector<double>> M1(lire_matrice()), M2(lire_matrice());
if (M1[0].size() != M2.size()) {
cout << "Multiplication de matrices impossible !" << endl;
} else {
cout << "Résultat :" << endl;
affiche_matrice(multiplication(M1, M2));
}
return 0;
}
// --------------------------------------------------------------------
vector<vector<double>> lire_matrice()
{
cout << "Saisie d'une matrice :" << endl;
// --------------------------------------------------------------------
vector<vector<double>> multiplication(const vector<vector<double>>& M1,
const vector<vector<double>>& M2)
{
// crée la Matrice prod à la bonne taille *et* l'initialise
// avec des zéros :
vector<vector<double>> prod(M1.size(), vector<double>(M2[0].size()));
return prod;
}
// --------------------------------------------------------------------
void affiche_matrice(const vector<vector<double>>& M)
{
for (auto ligne : M) {
for (auto element : ligne) {
cout << element << " ";
}
cout << endl;
}
}
Les deux changements à faire au code ci-dessus pour qu'il compile avec les anciennes versions des compilateurs (C++98)
sont
de séparer les deux ' >' dans les déclarations de matrices : remplacer partout
vector<vector<double>>
par
vector<vector<double> >
dans la derninère fonction ( affiche_matrice), de remplacer les itérations for C++11 par des itérations C :
Commentaires
Nous avons avec ce corrigé une belle illustration des progrès introduits dans C++11. En C++98, le code fourni produit en
effet plusieurs copies inutiles : les fonctions lire_matrice et multiplication créent leur propre matrice
(vector<vector<double>>), laquelle est recopiée en sortie (échange de l'information entre le return de la fonction
et son appel). Il y a donc à chaque fois deux matrices (vector<vector<double>>) : celle de l'instruction qui fait
l'appel et celle de la valeur de retour de la fonction appellé. Cela est inutile et coûteux (surtout si les matrices sont de grande
taille).
Une solution pour éviter cette duplication des matrices est, en C++98, d'utiliser les pointeurs (c'était d'ailleurs à l'époque
l'objet d'un exercice en soit).
Tout ceci n'est plus nécessaire en C++11. Cette nouvelle version du langage introduit en effet la notion de «sémantique de
déplacement» (move semantics), laquelle permet entre autres au compilateur, avec le MÊME code d'éviter ces copies
(techniquement c'est parce que les vector ont maintenant un «constructeur de déplacement» ( move constructor)). Cela
permet aux programmeurs d'écrire des codes plus efficaces, sans avoir rien de particulier à faire !
Vous pouvez même expliciter cette optimisation (mais ce n'est pas nécessaire, le compilateur devrait pouvoir le faire pour
vous), en transformant M1 et M2 en des «références vers des transitoires» ( r-value refences) :
(et c'est tout !!) Cela impose que les seules matrices existant dans tout le programme soient celles crées dans les deux appels
de lire_matrice et celle résultant de la multiplication. Plus aucune copie inutile supplémentaire...
Vive la move semantics !
[Niveau avancé] J'en profite pour pousser encore un peu plus loin pour ceux que cela intéresse (mais cela est vraiment
avancé, hors des objectifs du cours) : peut on encore faire mieux et ne pas introduire la 3 e matrice (le résultat de la
multiplication) ?
La réponse serait «oui» avec un autre algorithme pour le calcul de la multiplication permettant le calcul «sur place» (trop
compliqué pour être montré ici), mais imaginons que l'on veuille faire une addition au lieu d'une multiplication : on sait
qu'on peut alors faire cette addition «sur place», c'est-à-dire calculer le résultat de A+B dans A lui-même (ou dans B), sans
necessiter une troisième matrice pour stocker le résultat.
Peut-on coder cela en C++ ? c.-à-d. éviter de créer une 3 e matrice ( vector<vector<double>>) ? La réponse est «oui»
en C++11.
Il faut pour cela tout d'abord se rendre compte qu'on ne peut le faire que si A ou B sont des «transitoires» ( r-value reference).
En effet si A et B sont des «vraies données» (= des variables, l-values en termes techniques) alors on est obligé de créer une
3e matrice car on ne peut (souhaite) pas toucher à A ni à B. Il faut donc expliciter le cas où A ou B, ou les deux, sont des r-
value references. Cela nous conduit aux quatres prototypes suivants :
// deux données stockées, à préserver
Matrice addition(const Matrice& M1 , const Matrice& M2);
// deux "transitoires"
Matrice&& addition(Matrice&& M1 , Matrice&& M2 );
[fin de note])
Cependant, il est clair qu'on ne vas pas écrire quatre fois l'addition !! ( jamais de copier-coller !) Comment faire ?
Commençons par choisir un cas de référence, par exemple lorsque M1 est transitoire. Que faut-il faire alors ?
Simplement faire l'addition dans M1 et retransmettre M1 plus loin, toujours en tant que transitoire.
Il y a ici encore une subtilité : il faut savoir qu'une r-value reference nommée (par exemple en argument de fonction) devient
une l-value ! La fonction pour transformer une l-value en r-value pour la «passer plus loin» (cela la rend alors invalide dans
la suite locale, c.-à-d. dans la suite de sa portée), est la fonction move().
Le plus simple est sûrement lorsque B est transitoire et A non : il suffit d'inverser ! Attention cependant à ne pas oublier la
conversion automatique d'une r-value reference nommée en l-value reference !!
Pour le cas où les deux sont des transitoires : il suffit d'en déplacer une...
Le cas le plus compliqué pour réutiliser l'addition déjà écrite sans la réécrire est peut être le cas où A et B sont des l-values.
Dans ce cas, il est nécessaire d'introduire une 3e matrice pour ne pas les écraser. L'idée est alors simplement d'introduire
cette 3e matrice, d'y recopier l'une des deux autres (p.ex. A) puis de la «déplacer» dans l'appel de notre addition de référence.
// --------------------------------------------------------------------
Matrice&& addition(const Matrice& M1, Matrice&& M2)
{
return addition(move(M2), M1);
}
// --------------------------------------------------------------------
Matrice&& addition(Matrice&& M1, Matrice&& M2)
{
return addition(move(M1), M2);
}
// --------------------------------------------------------------------
Matrice addition(const Matrice& M1, const Matrice& M2)
{
Matrice resultat(M1); // copie M1 dans resultat pour...
return addition(move(resultat), M2); // ...utiliser la version r-value
}
La réponse est «peut être, mais sûrement oui». Si le compilateur que vous utilisez fait de l'élision de copie ( copy elision, une
optimisation du compilateur qui évite les copies superflues), alors on peut faire mieux (il est très certainement probable que
votre compilateur fasse de l'élision de copie (g++ le fait), mais ce n'est pas garantie par la norme avant C++17).
Si l'on peut compter sur l'élision de copie, alors on pourra ne faire qu' UNE seule version de l'addition qui prenne en compte
trois des 4 cas précédents (il faudra conserver la version addition(const vector<vector<double>>& M1,
vector<vector<double>>&& M2)) :
Notez qu'ici le premier argument est passé par valeur. Si cet argument est un temporaire, et si le compilateur fait de l'élision
de copie, cet argument ne sera pas copié mais déplacé (moved. Note : ceci est vrai car le type vector possède justement
cette sémantique. L'approche décrite ici ne fonctionnerait pas pour des objets n'ayant pas de «move semantic» et doit donc
être utilisée à bon escient !). De même, la copie de retour ne sera pas faite dans un contexte de temporaire (return value
optimization). Bref pour résumer, trois des 4 cas précédents seront correctement traités par cette seule fonction.
En conclusion, ce qu'il faut peut être retenir de tout ceci est que l'utilisation de la move semantics et des r-value references
n'est vraiment que pour de l'optimisation et qu'il vaut peut être mieux dans un premier temps ne pas y toucher explicitement,
mais se contenter d'utiliser (peut être même sans le savoir) ce qu'elle apporte déjà dans le langage de base.