Cours 1
Cours 1
Yann Thierry-Mieg
[email protected]
page 1
Plan des Séances
Objectif : principes de programmation d’applications concurrentes et réparties
Arc 1 : C++
1. Introduction C++ : bases, syntaxe
2. C++ : Allocation, Conteneurs
3. C++ : lib standard, itérateurs, lambdas
Arc 2 : Multi-thread
4. Programmation concurrente : threads, mutex
5. Synchronisations : atomic, conditions
Arc 3 : Multi-Processus
6. Multi-processus : fork, exec, signal
7. Communications interprocessus (IPC) : shm, sem, pipe, …
Arc 4 : Sockets
8. Communications distantes : Sockets
9. Protocoles de communication : Sérialisation, protobuf
Ouverture
10. Ouverture : introduction à MPI, CUDA, parallélisme grain fin… 2
Organisation
• 2h cours, 2h TD, 2h TME par semaine
• Examen réparti 1 (novembre) sur machines
• Examen réparti 2 (janvier) sur feuille
• Répartition : 10% note de TME, 40% Exam 1, 50% Exam 2.
3
Références
• Hypothèse / pré-requis :
• Niveau intermédiaire en C,
• Niveau confirmé en Java (POBJ, PRC en L3 ?)
• Références web :
• http://www.cplusplus.com/ (tutos, docs de références)
• https://fr.cppreference.com/ (des parties traduites, des parties en français)
• Une version en ligne du man : https://man.cx/
• StackOverflow : https://stackoverflow.com/
• Références biblio utiles :
• Stroustrup, « The C++ Programming Language », 4th Ed (C++11)
• Gamma, Helm, Vlissides, Johnson, « Design Patterns »
• Meyers, « Effective XXX » XXX=C++, STL, modern C++
• Cours en partie basé sur les supports de
• Denis Poitrenaud (P5), et Souheib Baarir (P10) sur le c++
• L. Arantes, P.Sens (P6) sur Posix 4
Le langage C++
5
Le Langage C++
• Sur-ensemble du C : gestion fine de la mémoire, du matériel
• Langage compilé : très efficace, pas de runtime
• Langage orienté objet : structuration des applications, variabilité
• Langage moderne en rapide évolution : C++11, 14, 17 et bientôt 20
• Fort support industriel : intel, microsoft, jeux…
• S’interface bien avec des langages front-end come Python
• Langage très versatile et puissant
Mais
• Langage relativement complexe, beaucoup de concepts
9
Compilation : sources
• Source divisés en :
• Header (.h, .hh, .hpp) : déclarations de types, de variables et de fonctions
• Source (.cpp, .cc) : corps des fonctions, définition des variables statiques
• On utilise #include pour inclure un source
• Gestion faite par le préprocesseur cpp
• Headers standards : <string>, <vector>, <iostream>
• Headers personnels : « MyClass.h », « util/Utility.h»
• Attention aux double include
#ifndef MYCLASS_H
#define MYCLASS_H #pragma once
// includes, déclarations Non standard mais OK
#endif gcc,Visual, clang…
• Sources plateformes indépendantes ?
• Lib standard OK, « stdunix.h » « windows.h »… NOK
• Le préprocesseur traite aussi les macros, dont on déconseille
l’usage dans l’UE. 10
Pré-processeur : cpp
Source Source
.cpp/.cc .cpp/.cc
include preprocesseur
// très long
Header //copie des headers
.h/.hh
include
11
Compilation : unité de compilation .o
• Chaque fichier source est compilé séparément
• Un fichier .cpp -> .o, fichier binaire plateforme dépendant
• Contient : le code des fonctions du cpp, de l’espace pour les static déclarés et
les littéraux (constantes, chaînes) du programme
• Référence indirectement les divers .h dans lequel il a trouvé les références
aux fonctions invoquées dans le code
• La compilation détecte un certain nombre de problèmes de syntaxe et de
typage
• La compilation cherche la déclaration adaptée, réalise les instanciations de
paramètres et vérifie le typage des invocations
• Concrètement on passe au compilateur
• un MyClass.cpp source
• -c pour arrêter la compilation avant le link
• -Wall pour activer les warnings
• -std=c++1y (selon compilo) pour le langage
• -g pour activer les symboles de debug : le .o garde des liens vers les sources12
Compilation : g++ -c
Librairie Dynamique
.so/.dylib/.dll
Ensemble congruent de .o
17
Hello world !
#include <iostream>
int main()
{
std::cout << "Hello World!" << std::endl;
}
18
Entrée/sorties
19
Types de base
20
Variables, structures de contrôle
#include <iostream> • Clause « using namespace »
using namespace std; • Les éléments de std deviennent
visible dans ::
int main ()
{ • Initialisation des variables
int a=5; // initial value: 5 • Syntaxe affectation = ou
int b(3); // initial value: 3 fonctionelle ()
int result; // no initial value • Pas de valeur par défaut /!\
a = a + b; • Opérations arithmétique et
result = a – 2 * b; priorités classiques (?)
cout << result; • std::ostream polymorphique,
accepte divers types
return 0;
} • Cas particulier pour char *
interprété comme une string du C
21
Opérateurs du C++
22
Opérateurs
• C++ possède un type bool
• Constantes true et false
• && and ; || or ; ! not
• Toute expression != 0 est vraie par promotion
• Les opérateurs sont nombreux et leur règles de priorité complexe
• Ne pas hésiter à sur-parenthèser un peu les expressions complexes
• Opérateurs new et delete ainsi que sizeof pour la gestion mémoire
• Opérateur de (cast) opère des conversions numériques
• Un sous ensemble de ces opérateurs peut être redéfini pour vos
propres types (classes)
23
std::string
La chaîne standard du c++, vient avec ses opérateurs.
24
Valeurs par défaut
25
Constantes
26
Constantes et Pointeurs
27
28
Références
29
30
31
32
33
34
35
36
37
38
La classe
39
Une classe : déclaration
class <nom de la classe> {
public:
<interface> => mode d’emploi de la classe
private:
<partie déclarative de l’implémentation>
};
page 40
Exemple : Pile en C++, utilisation de Classe
class Pile {
private:
enum {TAILLE = 10};
Données double tab[TAILLE];
privées unsigned int cpt; // nombre d’éléments empilés
public:
Pile (); // constructeur vide
Pile (double); // construit avec 1 élément
bool estPleine () const;
Interface
bool estVide () const;
publique
void empiler (double); /// @pre !estPleine()
double depiler (); /// @pre !estVide()
double getSommet () const; /// @pre !estVide()
};
page 41
Une classe : objet et fonctions membres
• Dès qu’une classe est déclarée, on peut instancier des objets (i.e. déclarer des
variables):
Pile p;
• Dès qu’un objet est instancié, on peut invoquer ces fonctions membres
publiques :
p. empiler (1.);
Pile * pp = &p;
pp->getSommet(); // via un pointeur
page 42
Une classe : implémentation (1/3)
• Pour qu’une classe soit complètement définie, il faut préciser le
code de toutes les méthodes apparaissant dans sa déclaration
#include "pile.h"
#include <iostream>
#include <cassert>
using namespace std;
// invariant de classe
// cpt >=0 && cpt <= TAILLE
page 43
Une classe : implémentation (2/3)
Pile::Pile () { // constructeur vide
cpt = 0; // this->cpt = 0;
assert (estVide()); // post-condition
}
page 44
Une classe : implémentation (3/3)
double Pile::depiler (){
assert (! estVide()); // pré-condition
return tab[--cpt];
}
bool Pile::estPleine () const {
return (cpt == TAILLE);
}
bool Pile::estVide () const {
return (cpt == 0);
}
double Pile::getSommet() const {
assert (! estVide()); // pré-condition
return tab[cpt -1];
}
page 45
Une classe : programme utilisateur
#include "Pile.h"
#include <iostream>
int main() {
Pile p;
p.empiler(4.5);
std::cout << p.getSommet();
}
page 46
Fonctions, Signature
47
Surcharge - Principe et motivation
• Lorsque plusieurs fonctions effectuent la même tâche
sur des objets de types différents, il est souhaitable de
leur donner le même nom.
Exemple:
void print(int); // affiche un entier
void print(const char *); // affiche une chaîne
48
Surcharge : résolution des conflits de noms (1/2)
• Lorsqu’une fonction f est appelée, le compilateur cherche à déterminer
quelle fonction de nom f est invoquée...Cette détermination est réalisée en
comparant le nombre et le type des paramètres effectifs avec le nombre et le
type des paramètres formels.
• Exemples :
void print(long);
void print(float);
49
Surcharge : résolution des conflits de noms (2/2)
• Deux fonctions ne peuvent pas être distinguées uniquement
par le type de la valeur retournée.
50
Surcharge : les méthodes des classes (1/2)
• Les méthodes d’une classe peuvent être surchargées.
• Exemple
class Nombre {
public:
void initialise(const char *);
void initialise(int i=0);
...
private:
int chiffre(int) const;
int& chiffre(int);
int tab[100];
};
51
Surcharge : les méthodes des classes (2/2)
void Nombre::initialise(const char *s) {
int l = strlen(s), i;
for (i=0; i<l; ++i) tab[i] = s[l-i-1]-'0';
for(; i<100; ++i) tab[i] = 0;
}
void Nombre::initialise(int i) {
char buff[100];
sprintf(buff, "%d", i);
initialise(buff);
}
int& Nombre::chiffre(int i) {
return tab[100-i];
}
52
Constructeur
53
Constructeur : motivation (1/2)
• Il est courant que l’utilisation d’un objet n’a de sens que si ce dernier est
préalablement initialisé
// fichier nombre.h
class Nombre {
public:
void initialise(const char *);
void affiche() const;
...
private:
int tab[100];
};
int main() {
Nombre n;
n.affiche(); // affiche n’importe quoi
}
54
Constructeur : motivation (2/2)
• Une bonne utilisation de la classe Nombre impose que tout objet de la classe
soit initialisé une et une seule fois avant son utilisation.
55
Constructeur : Définition
• Un constructeur est une méthode portant le même nom que la
classe.
56
Constructeur : exemple
class Nombre {
public:
Nombre(const char* s); int main() {
void affiche() const; Nombre n1( "12345678");
... Nombre n2; // erreur
private: ...
enum {MAX=100}; }
int tab[MAX];
};
Nombre::Nombre(const char* s){
int l = strlen(s), i;
assert(l<MAX);
for (i=0; i<l; ++i) {
tab[i] = s[l-i-1]-'0';
assert(tab[i]>=0 && tab[i]<=9);
}
for(; i<MAX; ++i)
tab[i] = 0;
}
57
Constructeur : le constructeur par copie
• Constructeur par copie (un seul paramètre de même type que
l’instance courante).
• Exemple :
Nombre::Nombre(const Nombre& n) {
for(int i=0; i<MAX; ++i)
tab[i] = n.tab[i];
}
59
Constructeur : le constructeur vide
• Constructeur vide (sans paramètre) :
✓ Exemple
Nombre::Nombre() {
for(int i=0; i<MAX; ++i)
tab[i] = 0;
}
60
Construction et allocation dynamique : rappels
➢ Les fonctions d’allocation mémoire.
61
Construction et allocation dynamique : exemples
int *pi = new int;
*pi = 12;
delete pi;
62
Construction et allocation dynamique : les objets
• Un objet alloué dynamiquement peut être initialisé par un appel à un
constructeur
int main() {
Nombre* p;
63
Initialisation objet membre d’une classe (1/2)
• Comment initialiser (invoquer le constructeur) d’un objet
membre d’une classe (i.e. un objet emboîté) ?
class Compte{
public:
Compte( const string& n,
const Nombre& m,
const Nombre& d);
...
private:
string nom;
Nombre mont;
Nombre dec;
};
64
Initialisation objet membre d’une classe (2/2)
65
Exemple : La pile
66
Exemple : une pile d’entiers (1/4)
class Stack {
public:
Stack(int cap); // cap = capacité initiale
void push(int i); // empiler i
bool empty() const; // pile vide?
int top() const; // sommet de la pile?
void pop(); // dépiler
private:
enum {DEFAULT_CAPACITY = 10, FACTOR = 2};
int *stack; // tableau dynamique
int head; // indice du sommet de la pile
int capacity; // capacité de la pile
};
67
Exemple : une pile d’entiers (2/4)
Stack::Stack(int cap) {
assert(cap > 0);
capacity = cap;
stack = new int[capacity];
Allocation initiale
head = 0;
}
void Stack::push(int i) {
if (head == capacity) {
Lorsque c’est
capacity *= FACTOR;
int *tmp = new int[capacity]; nécessaire, la
for (int j = 0; j < head; ++j) capacité est
tmp[j] = stack[j]; doublée
delete [] stack;
stack = tmp;
}
stack[head++] = i;
}
68
Exemple : une pile d’entiers (3/4)
bool Stack::empty() const {
return head == 0;
}
void Stack::pop() {
assert(!empty());
--head;
}
69
Exemple : une pile d’entiers (4/4)
Représentation en mémoire
Pile d’exécution Tas
int main() { 0 1 2
s
Stack s(3); stack
// (1) (1) head 0
capacity 3
0 1 2
for (int i=4; i>1; --i) s
stack 4 3 2
s.push(i);
// (2) (2) head 3
capacity 3
0 1 2 3 4 5
s.push(1); s
4 3 2 1
stack
// (3)
return 0; (3) head 4
capacity 6
}
La classe : copie, affectation,
destruction
Affectation et construction par copie
• Pour toute classe, l’affectation entre instances de même type et la
construction par copie d’instance sont toujours possibles
int main() {
Stack s1(3);
Stack s2(3);
s2.push(1);
s2 = s1; // affectation
75
Construction par copie
Stack(const Stack& s);
• C’est une construction.
• En conséquence, nous sommes assuré que l’objet qui doit être
initialisé est une nouvelle variable du programme.
• Il est donc suffisant d’initialiser les champ de ce nouvel objet en
dupliquant les données dynamiquement allouées
Stack::Stack(const Stack& s) {
capacity = s.capacity;
// duplication du tableau
stack = new int[capacity]; Duplication
for (head = 0; head < s.head; ++head) du tableau
stack[head] = s.stack[head];
}
76
Opération d’affectation
Stack& operator=(const Stack& s);
• C’est une instruction.
• En conséquence, nous sommes assuré que l’objet qui doit être
affecté est une variable du programme qui a été préalablement
construite.
• Il est donc nécessaire de désallouer les données créées
dynamiquement puis de ré-initialiser les champ en dupliquant les
données dynamiquement allouées.
• Autres points à prendre en compte:
✓ L’auto-affectation
Stack s(3);
s = s; // le détecter et ne rien faire
✓ L’enchaînement d’affectation
Stack s1(3), s2(3), s3(3);
s1 = s2 = s3; // impose de renvoyer un résultat
77
Opération d’affectation (suite)
78
Solution aux fuites mémoire
• Chaque fois qu’une instance est supprimée, il faut désallouer ce
qui a été créé dynamiquement.
• C’est le rôle des destructeurs.
• Il sont appelés implicitement dès qu’une instance est supprimée,
c’est-à-dire :
✓ Lorsqu’une fonction se termine, les variables locales sont supprimées
✓ Lorsqu’une instance créée dynamiquement est désallouée.
79
Destruction
~Stack();
Stack::~Stack() {
delete [] stack; Désallocation du tableau
}
80
Classe C++ canonique
81
Forme canonique d’une classe
• Toute classe faisant de l’allocation dynamique de mémoire « doit »
comporter :
✓ Un constructeur par copie
✓ Un opérateur d’affectation
✓ Un destructeur
Une classe respectant ces règles est dites “sous forme canonique”
82
Gestion des objets
• Les opérateurs de « move » permettent de transférer le contenu
d’un temporaire (rvalue). Le temporaire va mourir tout de suite, on
lui prend sa mémoire et on le vide.
83
L’amitié
Toute fonction peut être déclarée «amie» d’une (ou plusieurs)
classe(s)
Une fonction « amie » d’une classe peut accéder directement (sans
passer par des méthodes) aux éléments privés de la classe
Exemple
class Personne { La fonction
public: afficher est
... «amie» de la
friend void afficher(const Personne& p);
private: classe Personne
char nom[20]; C’est une fonction et
int age; non pas une méthode
};
87
Les opérateurs
Le C++ autorise la surcharge des opérateurs
switch (i) {
case 1: return x;
case 2: return y;
case 3: return z;
}
}
// Addition de 2 Vecteurs
Vecteur3d operator+(const Vecteur3d& v1,
const Vecteur3d& v2) {
int i, j, k;
i = v1.get(1) + v2.get(1);
j = v1.get(2) + v2.get(2);
k = v1.get(3) + v2.get(3);
return Vecteur3d(i,j,k);
}
for (int i=1; i<=3; i++) cout << c.get(i) << " " ;
cout << endl;
for (int i=1; i<=3; i++) cout << d.get(i) << " " ;
cout << endl;
return 0;
}
Surcharge par une méthode
class Vecteur3d {
public:
Vecteur3d(int x=0, int y=0, int z=0);
int get(int i) const;
int operator*(const Vecteur3d& v) const;
private:
Opérateur binaire:
int x, y, z;
};
• l’opérande gauche est l’instance courante
• l’opérande droite est le paramètre
Vecteur3d operator+(const Vecteur3d& v1, const Vecteur3d& v2);
// multiplication de 2 Vecteurs
int Vecteur3d::operator*(const Vecteur3d& v) const {
int res;
return res;
}
La méthode (l’opérateur) peut être invoquée de 2 façons
// affichage
ostream& operator<<(ostream& os, const Vecteur3d& v)
{
for (int i=1; i<=3; i++)
os << v.get(i) << ' ';
return os;
}
// Utilisation de la classe Vecteur3d
void main() {
Vecteur3d a(5, 10, 15);
Vecteur3d b;