généricité, nouveautés
Gérard Rozsavolgyi
roza@univ-orleans.fr
Jan 2022
M4105C Gérard Rozsavolgyi, basé sur l’ancien cours de sylvain.jubertie. Plan :
8 semaines :
C with classes
, langage
standardisé g++ -o hello hello.cpp
Exécution :
./hello
g++ -std=c++20 hello.cpp -o hello
g++ -std=c++20 -O2 -Wall -pedantic -pthread -o main main.cpp
Avec affichage des warnings et violations des règles du standard utilisé, et usage de la librairie pthreads.
make hello
(fichier unique)
Ajouter :
CXXFLAGS += -std=c++20
Dans CMakeList.txt, mettre en premier :
#include <iostream>
#include <string>
#include <vector>
template<typename T>
std::ostream& operator<<(std::ostream& os, const std::vector<T>& vect)
{
for (auto& elt : vect){ os << elt << ' '; }
return os;
}
int main()
{
std::vector<std::string> vect = {"Hello", "from", "GCC", __VERSION__, "!" };
std::cout << vect << std::endl;
}
Initialisation par affectation
Initialisation par constructeur
Initialisation uniforme (C++11)
const
n’a pas à être utilisé.Le mot clé static devant une variable permet de modifier :
Contient beaucoup de choses pour manipuler les objets fondamentaux :
std::string
std::iostream
std::array
etc.Déclaration :
std::array
(cf. section
Bibliothèque standard) à la place des tableaux statiques C.Type complexe std::string
de la bibliothèque
standard.
#include <iostream>
#include <chrono>
long fibo(unsigned n){ if (n < 2) return n; return fibo(n-1) + fibo(n-2); }
int main()
{
auto start = std::chrono::steady_clock::now();
std::cout << "f(42) = " << fibonacci(42) << '\n';
auto end = std::chrono::steady_clock::now();
std::chrono::duration<double> elapsed_seconds = end-start;
std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
}
Par exemple pour afficher proprement une date. (sous Windows uniquemement pour le moment …)
auto now = std::chrono::floor<std::chrono::milliseconds>(std::chrono::system_clock::now());
std::cout << std::format("{0:%d.%m.%Y %H:%M:%S}", now);
ou
auto now = std::chrono::system_clock::now();
std::cout << std::format("{0:%d.%m.%Y %H:%M:%S}", std::chrono::floor<std::chrono::milliseconds>(now));
on peut aussi utiliser le raccourci %T = %H:%M:%S
Les fonctions C++ ressemblent à celles du C avec quelques particularités …
Le mot clé const peut être appliqué aux arguments d’une fonction de différentes manières. - types simples - pointeurs - références
void fct( int const a ); // a: copie non modifiable.
void fct( int const ✯ pa ); // copie du pointeur modifiable
// mais valeur pointée non modifiable.
void fct( int ✯ const pa );
// copie du pointeur non modifiable mais valeur pointée modifiable.
void fct( int const ✯ const pa ); // rien n’est modifiable.
void fct( int & a ) // Passage par référence, a modifiable.
void fct( int const & a ) // Passage par référence, a non modifiable.
Deux méthodes de définitions possibles :
Attributs privés (par défaut mais bien de l’expliciter)
Exemple : attributs publics
Un constructeur peut en appeler un autre (C++11). Exemple
Exemple d’appel :
new
et delete
sont utilisés
pour allouer et libérer de la mémoire dans le contexte du C++.malloc
et free
du C. (ne pas mélanger …)la copie par défaut effectue uniquement la copie du pointeur, du descripteur de fichier, … Pour effectuer une copie complète, deep copy, il est obligatoire de surcharger le constructeur de recopie pour effectuer la copie des données pointées.
class A {
. . .
int ✯ p i ;
A() {
p i = new int ;
};}
. . .
A a;
A b( a ); // b.p i pointe vers la même donnée que a.p i
✯(a. p i ) = 42;
std :: cout << ✯(b. p i ) << std :: endl ; // affiche 42...
Mais double free corruption
à la sortie du programme
…
Pour effectuer la deep copy d’une instance il convient de mettre la classe dans la forme canonique de Coplien qui impose que la classe possède :
Exemple
Les méthodes ne modifiant pas l’état de l’instance doivent être déclarées constantes (pas obligatoire mais…). Exemple
En plus de pouvoir se placer devant des variables et des fonctions (C), le modificateur static peut être placé devant :
Accès à des attributs statiques : appel par nom complet
+, -, ✯, =, [] , (),
…Cf. :
Cela permet de simplifier l’écriture mais doit être utilisé à bon
escient et de manière pertinente (attention aux progs illisibles !!!).
Dans ce cas l’opérateur s’applique entre l’instance courante et celle passée en argument.
L’instance courante prend le contenu de son argument et est retournée.
Cela permet d’écrire : a = b = c = ...;
Dans le cas de la surchage pour l’affichage, l’opérateur modifie et
retourne le flux std::ostream
passé en paramètre :
const
.&
.Dans un .cpp :
Déclaration dans le .cpp
auto
: inférence de type (C++11)auto
permet d’attribuer un type en le
déduisant automatiquement.Le compilateur peut déterminer le type de b
en fonction
de celui de a
, ou le type de x
en fonction du
type de retour de la fonction fct
:
Par contre on ne peut pas écrire : auto x;
auto
déduit uniquement le type, pas les modificateurs
:
auto
peut aussi déduire le type de retour d’une fonction
:
Le spécificateur decltype( expr )
permet de récupérer le
type d’une expression donnée en argument.
if
est comme en C mais il y a des subtilités …switch
pour aiguillage.for
peut se faire classiquement mais pas mal d’autres
choses possibilités (range based loops, auto, etc.)Comme en C :
if constexpr
: C++17 Lorsque l’expression peut être
évaluée à la compilation, seul le code du bloc if ou else est généré par
le compilateur :
Exemple
Comme en C :
Comme en C :
Exemple :
On a déjà aperçu ces objets permettant de représenter les flux fondamentaux du système.
#include<fstream>
#include<iostream>
int main(){
std::cout<<"Ecriture dans fic.tx :"<<std::endl;
std::ofstream ofs("fic.txt");
ofs << "Hello le monde !\n";
ofs.close();
std::cout<<"Relecture de fic.txt" <<std::endl;
std::ifstream ifs("fic.txt");
std::string s;
ifs >> s;
ifs.close();
std::cout << s << std::endl;
}
L’un des points délicats en C comme en C++
Exemple :
Erreurs communes :
Les smart pointers (pointeurs intelligents) s’occupent de la gestion de certains types de pointeurs et de la libération de la mémoire les concernant, tout en conservant la syntaxe habituelle des pointeurs.
unique_ptr
: pour un pointeur qui ne doit pas être
partagé,shared_ptr
: pour un pointeur partagé,weak_ptr
: pour un pointeur temporaire sur un shared
pointer. unique_ptr< Student > ps0( new Student( ... ) );
// ou : auto ps0 = std::make_unique<Student>();
std :: cout << ✯p s0 << std :: endl ;
//auto ps1 = ps0; // Pas de constructeur de recopie.
// ps1 prend la propriété, ps0 devient invalide.
unique_ptr< Student > p s1( std ::move( p s0 ) );
// std::cout << *ps0 << std::endl; // Erreur segmentation.
ps1->setFirstName( ”Hector” );
shared_ptr< Student > ps0( new Student( ... ) );
// ou : auto ps0 = std::make_shared<Student>( ... );
std :: cout << ✯ps0 << std :: endl ;
auto ps1 = ps0;
std :: cout << ps1.use_count () << std :: endl ;
ps1->setFirstName( ”Hector” );
std :: cout << ✯ps0 << std :: endl ;
std :: cout << ✯ps1 << std :: endl ;
L’usage des templates apporte une grande puissance au C++, au prix d’une certaine technicité syntaxique …
La généricité consiste à développer un code ou une structure de données avec des types non explicitement définis i.e. génériques.
std::max
,std::vector
.On parle alors de méta-fonctions ou de méta-classes qui vont être instanciées et produire respectivement des fonctions ou des classes. Ces méta-fonctions et méta-classes peuvent être paramètrées par des types primitifs ou complexes et par des valeurs entières.
template < typename... TS >
std :: tuple
par exemple. // Fin de la récursion.
template < typename T >
void printer( T const & t ) {
std :: cout << t << std :: endl ;
}
// Cas général.
template < typename T, typename... TS >
void printer( T const & t , TS const &... ts ) {
std :: cout << t << ”, ”;
printer( ts ... );
}
// Utilisation.
printer( 1.0f , 3, ’a ’ , ”Hello !” );
La Standard Template Library apporte un framework dédié au maniement des collections basé sur l’usage des templates. La STL est un framework essentiel du C++ qui a beaucoup évolué avec les versions récentes de C++
Concepts :
std :: array
std :: vector
std :: deque
std :: forward list
std :: list
std :: set
std :: map
[capture list ] (params) code;
.[]
: pas de capture.[&]
: capture des variables externes à la lambda
par référence.[=]
: capture par valeur.[a]
: capture de la variable a par valeur.[&a]
: capture de la variable a par référence.std::ref()
.std::mutex
. // Fonctions à appeler.
void fct0 () { ... }
void fct1( int n ) { ... }
void fct2( int & n ) { ... }
std :: thread th0( fct );
std :: thread th1( fct , 5 );
int a = 5;
std :: thread th2( fct , std :: ref ( a ) );
std :: thread th3( []() { std :: cout << ”Lambda!\n”; });
th0 . join ();
th1 . join ();
th2 . join ();
th3 . join ();
class B : public/protected/private A
public : /* les membres publics de A restent publics :
accessibles par l’héritier i.e. dans la classe B,
accessibles à partir des instances de B.*/
protected : /* les membres publics de A deviennent protected :
accessibles dans la classe B,
inaccessibles à partir des instances de B.*/
private : /* Les membres publics et protected deviennent privés :
inaccessibles dans la classe B,
inaccessibles à partir des instances de B.*/
### Problème d’héritage (suite)
int main() {
std :: unique_ptr< Tool > screwdriver
= std :: make_unique< Screwdriver >();
screwdriver->display (); // affichage ?
}
-> Résultat : This is a tool
std:: unique_ptr< Tool >
.virtual
: définition d’une méthode virtuelle qui peut
être surchargée par les héritiers.virtual ... =0
: méthode virtuelle pure i.e. la classe
contenant cette méthode ne peut être instanciée.override
: indique que la méthode doit surcharger la
même méthode (même prototype) de l’ancêtre.final
: indique que la méthode ne peut plus être
surchargée par les héritiers. struct Tool {
virtual void display () {
std :: cout << ”This is a tool .\n”;
};
struct Screwdriver : public Tool {
void display () {
std :: cout << ”This is a screwdriver .\n”;
};
int main() {
std :: unique_ptr< Tool > screwdriver = std :: make_unique< Screwdriver > ();
screwdriver->display (); // This is a screwdriver.
}
virtual
indique que la méthode n’est plus appelée
directement.Tool
, la table contient un
pointeur vers la méthode Tool:: display
.Screwdriver
, la table
contient un pointeur vers la méthode
Screwdriver :: display
.std:: unique_ptr< Tool >
est bien attribué à
l’instance.Screwdriver :: display
.display
est appelée sur le
type Tool
mais c’est le pointeur de la table virtuelle qui
est utilisé.Le polymorphisme engendre un surcoût :
delete
est appelé sur le type déterminé statiquement,
ici Tool
. Donc le destructeur de la classe
Screwdriver
n’est pas appelé : fuite
mémoire. Solution : rendre le destructeur
virtuel.
### Interfaces
Une interface est une classe contenant uniquement des
méthodes virtuelles pures. Il n’y a pas de mot-clé spécifique
comme interface en Java. Exemple :
c++ class UneInterface { public : virtual method( ... ) = 0; };
Une classe est abstraite si au moins une de ses méthodes est virtuelle pure :
struct Tool { // Abstract class.
std :: string name;
virtual void use() = 0;
virtual void display () {
std :: cout << ”This is a ” << name;
};}
struct Screwdriver : public Tool {
std :: string type ;
Screwdriver( std :: string type ) type( type ) { ... }
void use() override { ... }
void display () {
Tool :: display ();
std :: cout << ” of type ” << type ;
} };
templates
et de
constexpr
.Total : 3 vec3f temporaires + 4 boucles for.
Total : pas de vec3f temporaire, 1 boucle for.
Surcharge de l’opérateur + pour les expressions : plus de calcul, construction d’une expression.
Définition de l’opération à effectuer.
Exemple
Utilisation de boost::property tree
.
Lecture d’un fichier json :
json { "login": "jubertie", "server_name": "192.168.0.254", "array": [ "1", "2", "7" ] }
boost::property tree
boost :: property tree :: ptree p;
boost :: property tree :: read json ( ”config . json” , p );
auto login = p.get( ”login”, ”none” );
auto server name = p.get( ”server name”, ”localhost” );
auto missing = p.get( ”missing”, ”missing” );
std :: cout << ”login=” << login << std :: endl ;
std :: cout << ”server name=” << server name << std :: endl ;
std :: cout << ”missing=” << missing << std :: endl ;
for( auto x: p. get child ( ”array” ) ) {
std :: cout << x. second . get value< int >() << std :: endl ;
}
auto
namespace X::Y { ... }
au lieu de
namespace X { namespace Y { ... } }
if constexpr(expr)
if( init ; expr )
filesystem
#include
par
import
:#ifdef ... HPP #define
pour les fichiers . hpp.