C++ moderne

Ce document en PDF : c++-moderne.pdf


C++

C++ est un langage de programmation compilé permettant la programmation sous de multiples paradigmes (comme la programmation procédurale, orientée objet ou générique). Ses bonnes performances, et sa compatibilité avec le C en font un des langages de programmation les plus utilisés dans les applications où la performance est critique.

Créé initialement par Bjarne Stroustrup dans les années 1980, le langage C++ est aujourd’hui normalisé par l’ISO. Sa première normalisation date de 1998 (ISO/CEI 14882:1998), ensuite amendée par l’erratum technique de 2003 (ISO/CEI 14882:2003). Une importante mise à jour a été ratifiée et publiée par l’ISO en septembre 2011 sous le nom de ISO/IEC 14882:2011, ou C++11. Depuis, des mises à jour sont publiées régulièrement : en 2014 (ISO/CEI 14882:2014 ou C++14) puis en 2017 (ISO/CEI 14882:2017 ou C++17). [source : wikipedia.org]

Les changements du langage C++ concernent aussi bien le langage initial que la bibliothèque standard.

Remarque : La bibliothèque standard du C++ (C++ Standard Library) est une bibliothèque de classes et de fonctions standardisées selon la norme ISO pour le langage C++. Elle contient aussi la bibliothèque standard du C. Une des principales briques de la bibliothèque standard du C++ est sans aucun doute la STL (Standard Template Library), à tel point qu’il y a souvent confusion entre les deux.

Dans l’index de popularité des langages TIOBE, le C représente 16,2 % (première place) et le C++ 7,6 % (quatrième place) en novembre 2020.

Liens :

C++ moderne

Le C++ moderne est apparu avec la mise à jour C++11.

Les standards C++11, 14, 17 et bientôt 20 apportent de nombreuses fonctionnalités : gestion automatique de la mémoire via des pointeurs intelligents (Smart Pointers), déduction de type automatique à la déclaration via auto, etc …

L’implémentation des standards C++ va dépendre du compilateur utilisé, de sa version et des options invoquées.

Exemple sous Ubuntu 18.04 :

$ g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
$ man g++
...
g++ [-std=standard] ...
...
-std=
          Determine the language standard. This option is currently only supported when compiling C or C++.
...
c11
c1x
iso9899:2011
          ISO C11, the 2011 revision of the ISO C standard.  This standard is substantially completely supported, modulo bugs, floating-point issues (mainly but not entirely relating to optional C11 features from Annexes F and G) and the optional Annexes K (Bounds-checking interfaces) and L (Analyzability). The name c1x is deprecated.
c++11
c++0x
          The 2011 ISO C++ standard plus amendments. The name c++0x is deprecated.
c++14
c++1y
          The 2014 ISO C++ standard plus amendments. The name c++1y is deprecated.
gnu++14
gnu++1y
          GNU dialect of -std=c++14. This is the default for C++ code. The name gnu++1y is deprecated.
c++1z
          The next revision of the ISO C++ standard, tentatively planned for 2017. Support is highly experimental, and will almost certainly change in incompatible ways in future releases.               

Remarque : il est possible de définir le standard utilisé pour le compilateur C++ dans un projet Qt. Pour cela, il suffit de l’indiquer dans la variable CONFIG de son fichier .pro :

CONFIG += c++11

Les exemples

https://github.com/tvaira/cpp-moderne

C++11

Lien : C++11

Les types et les variables

Le langage C++ est dit fortement typé. Chaque variable possède un type.

Une définition est composée de :

  • un type pour définir la convention d’interprétation des valeurs possibles
  • un objet qui contient en mémoire la valeur d’un type
  • une valeur
  • une variable qui est le nom de l’objet

Chaque type est directement lié à une architecture matérielle et possède une taille fixe. La taille d’un objet et/ou d’un type est obtenue avec l’opérateur sizeof.

L’opérateur typeid() (dans type_info) permet lui d’obtenir le type d’une valeur à l’exécution :

Remarque : Il existe un moyen d’insérer des chaînes de caractères complexes dans le code source sans le formatter avec R"(raw_string)". Ceci est pratique avec des chaînes qui contiennent des guillemets " et/ou des antislash \.

Lien : string_literal

Initialisation

Avant d’être utilisé, un objet doit être initialisé. Il existe l’opérateur =, les crochets {} ou les parenthèses () comme initialiseurs universels :

Remarque : Le nouveau standard ISO a introduit une syntaxe d’initialisation uniforme avec les accolades {}.

En C++03, il est possible d’assigner une valeur par défaut aux attributs statiques et constantes directement dans le fichier d’en-tête. C++11 étend cette possibilité aux attributs des classes :

auto

Il est aussi possible de laisser le compilateur déduire le type à la compilation en utilisant le mot-clé auto :

Membre mutable

Dans une fonction const, il est impossible de modifier un attribut (une variable membre) sauf si ce membre est préfixé du mot-clé mutable.

Remarque : Un membre mutable n’est jamais const !

Lien : mutable specifier

Les pointeurs

Il faut maintenant utiliser nullptr à la place de 0 ou NULL pour initialiser un pointeur :

Les pointeurs intelligents

Un pointeur intelligent (smart pointer) est un type abstrait de données qui simule le comportement d’un pointeur en y ajoutant des fonctionnalités telles que la libération automatique de la mémoire allouée ou la vérification des bornes.

En C++11, les pointeurs intelligents sont implémentés à l’aide de templates qui “imitent” le comportement des pointeurs grâce à la surcharge des opérateurs, tout en fournissant des algorithmes de gestion mémoire.

  • unique_ptr est une classe qui possède un membre qui pointe sur une ressource (objet) non partageable. unique_ptr gère l’objet pointé en devenant responsable de sa suppression lorsqu’il passe hors de portée.

Lien : unique_ptr

  • Les shared_ptr implémentent le comptage de références, ce qui permet de partager l’objet possédé par un shared_ptr entre plusieurs shared_ptr sans se soucier de comment libérer la mémoire associée. Lorsque le dernier shared_ptr est détruit, l’objet pointé est également détruit.

Lien : shared_ptr

  • Les weak_ptr permettent de voir et d’accéder à une ressource (objet) possédée par un shared_ptr mais n’ont aucune influence sur la destruction de ce dernier. Ils servent principalement à s’affranchir du problème des références circulaires.

Lien : weak_ptr

Les énumérations

De manière générale, les énumérations permettent de grouper des ensembles de valeurs dans un type distinct.

Il y a quelques limitations (donc problèmes !) dans l’utilisation du type enum :

C++11 a introduit des classes enum (appelées énumérations étendues) qui rendent les énumérations fortement typées. L’énumération de classe ne permet pas la conversion implicite en int et ne compare pas non plus les énumérateurs de différentes énumérations.

Lien : enum

Syntaxe :

enum class name { enumerator = constexpr , enumerator = constexpr , ... } // constexpr = 0 par défaut
enum class name : type { enumerator = constexpr , enumerator = constexpr , ... } 
enum class name ; // int par défaut
enum class name : type ;

Exemple :

decltype

Le mot-clé decltype, introduit dans C++11, permet de définir une expression pour exprimer une déclaration de type. decltype « retourne » un type.

Lien : decltype

Remarque : decltype est notamment intéressant dans l’écriture de bibliothèques génériques à base de templates. Sinon il est fort probable que vous n’ayez pas à vous en servir.

Les littéraux utilisateur

C++ fournit un certain nombre de littéraux. Les caractères 12.5 sont un littéral qui est résolu par le compilateur comme un type double. Avec l’ajout du suffixe f (12.5f) le compilateur interprétera la valeur comme un type float. Les modificateurs de suffixe (comme U pour unsigned ou L pour long) pour les littéraux sont fixés par la spécification C++.

À partir de C++11, il est possible de définir ses propres littéraux afin de fournir des suffixes syntaxiques qui améliore la lisibilité et renforce la sécurité des types.

Lien : user_literal

La bibliothèque standard a elle-même défini des littéraux pour std::complex et pour les unités dans les opérations de temps dans std::chrono :

Liens :

C++ 11 permet donc à l’utilisateur de définir de nouveaux types de modificateurs littéraux qui construiront des objets basés sur la chaîne de caractères que le littéral modifie.

La transformation des littéraux est redéfinie en deux phases distinctes : raw (brut) et cooked (préparé). Un littéral raw est une séquence de caractères d’un type spécifique, tandis que le littéral cooked est d’un type distinct. Le littéral 1234, en tant que littéral raw, est la séquence de caractères ‘1’, ‘2’, ‘3’ et ‘4’. En tant que littéral cooked, il s’agit de l’entier 1234. Le littéral 0xA est ‘0’, ‘x’, ‘A’ soit l’entier 10.

Liens :

Tous les littéraux définis par l’utilisateur seront des suffixes. La définition de littéraux de préfixe n’est pas possible. Tous les suffixes commençant par n’importe quel caractère sauf le trait de soulignement (_) sont réservés par la norme. Ainsi, tous les littéraux définis par l’utilisateur doivent avoir des suffixes commençant par un trait de soulignement (_).

Les littéraux utilisateur sont définis via un opérateur littéral qui se nomme operator "". en.wikipedia.org

Pour les littéraux numériques, le type du littéral est unsigned long long pour les littéraux entiers ou long double pour les littéraux à virgule flottante. (Remarque : il n’est pas nécessaire d’utiliser des types intégraux signés car un littéral avec un préfixe de signe est analysé comme une expression contenant le signe en tant qu’opérateur de préfixe unaire operator -, qu’il est possible de surcharger, et le nombre non signé.)

On va définir une classe Temperature. Il sera alors possible de définir un littéral pour les degrés Celsius et un autre pour les Fahrenheit. Ensuite, on sera forcé d’exprimer explicitement l’unité de mesure en écrivant par exemple : auto t1 = 36.5_celsius ou auto t2 = 32.0_fahrenheit.

#include <iostream>

using namespace std;

class Temperature
{
    private:
        long double temperature = { 0 }; // en celsius
        explicit Temperature(long double valeur) : temperature(valeur) { }
        friend Temperature operator"" _celsius(long double valeur); // pour une valeur en virgule flottante
        friend Temperature operator"" _celsius(unsigned long long valeur); // pour une valeur entière
        friend Temperature operator"" _fahrenheit(long double valeur);
        friend Temperature operator"" _kelvin(long double valeur);

    public:
        constexpr static long double zero_absolu = 273.15; // en celsius
        
        long double celsius() { return temperature; }
        long double fahrenheit() { return (temperature*9./5.) + 32.; }
        long double kelvin() { return (temperature + Temperature::zero_absolu); }
        
        Temperature operator+(Temperature t)
        {
            return Temperature(celsius() + t.celsius());
        }
        friend Temperature operator-(Temperature t);
};

Temperature operator"" _celsius(long double valeur) // pour une valeur en virgule flottante
{
    return Temperature(valeur);
}

Temperature operator"" _celsius(unsigned long long valeur) // pour une valeur entière
{
    return Temperature(double(valeur));
}

Temperature operator"" _fahrenheit(long double valeur)
{
    return Temperature((5./9.) * (valeur - 32.));
}

Temperature operator"" _kelvin(long double valeur)
{
    return Temperature(valeur - Temperature::zero_absolu);
}

Temperature operator-(Temperature t)
{
    return Temperature((-1.) * t.celsius());
}

int main()
{
    Temperature zeroCelsius = 32._fahrenheit; //Temperature zeroCelsius = 0_celsius;
    cout << "zeroCelsius = " << zeroCelsius.celsius() << "C " << zeroCelsius.kelvin() << "K " << zeroCelsius.fahrenheit() << "F " << endl;

    Temperature zeroAbsolu = 0._kelvin;
    cout << "zeroAbsolu = " << zeroAbsolu.celsius() << "C " << zeroAbsolu.kelvin() << "K " << zeroAbsolu.fahrenheit() << "F " << endl;

    Temperature t1 = 36.0_celsius + 42.0_celsius;
    cout << "t1 = 36.0_celsius + 42.0_celsius" << endl;
    cout << "t1 = " << t1.celsius() << "C " << t1.kelvin() << "K " << t1.fahrenheit() << "F " << endl;

    Temperature t2 = 36.0_celsius + -42.0_celsius;
    cout << "t2 = 36.0_celsius + -42.0_celsius" << endl;
    cout << "t2 = " << t2.celsius() << "C " << t2.kelvin() << "K " << t2.fahrenheit() << "F " << endl;

    auto t3 = 36.0_celsius;
    cout << "t3 = " << t3.celsius() << "C " << t3.kelvin() << "K " << t3.fahrenheit() << "F " << endl;

    auto t4 = 36_celsius;
    cout << "t4 = " << t4.celsius() << "C " << t4.kelvin() << "K " << t4.fahrenheit() << "F " << endl;

    // Evidemment, ceci n'est plus possible :   
    //Temperature t5 = 25; // error: conversion from 'int' to non-scalar type 'Temperature' requested
    //Temperature t5 = 25.; // error: conversion from 'double' to non-scalar type 'Temperature' requested
    //Temperature t5 = 36_fahrenheit; // error: unable to find numeric literal operator 'operator""_fahrenheit' -> il faudrait donc surcharger operator"" _fahrenheit(unsigned long long valeur) 
    
    return 0;
}

Range-for

Introduit en C++11, la boucle Range-for exécute une boucle for sur une plage de valeurs, telles que tous les éléments d’un conteneur.

Lien : range-for

Les expressions rationnelles

La bibliothèque C++ standard prend maintenant (en C++11) en charge les expressions rationnelles (Regular Expressions) avec l’en-tête <regex> via une série d’opérations :

  • regex_match : correspondance exacte avec une expression rationnelle ;
  • regex_search : recherche correspondance avec une expression rationnelle ;
  • regex_replace : recherche correspondance avec une expression rationnelle et la remplace ;

Liens :

Délégation du constructeur

En C++03, un constructeur appartenant à une classe ne peut pas appeler un autre constructeur de cette même classe, ce qui peut entraîner de la duplication de code lors de l’initialisation de ses attributs. En permettant au constructeur de déléguer la création d’une instance à un autre constructeur, C++11 apporte donc une solution.

Héritage des constructeurs

En C++03, les constructeurs d’une classe de base ne sont pas hérités par ses classes dérivées. C++11 permet d’hériter explicitement des constructeurs de la classe de base grâce à l’instruction using :

Liste d’initialiseurs

C++11 introduit le patron de classe std::initializer_list qui permet d’initialiser les conteneurs avec une suite de valeurs entre accolades.

Lien : initializer_list

constexpr

Le mot clé constexpr a été introduit dans C++11 et amélioré en C++14. constexpr déclare un objet utilisable dans ce que la norme appelle des expressions constantes.

Comme const, constexpr peut être utilisé sur des variables mais aussi des fonctions et des constructeurs.

Lien : constexpr

Les nouveaux spécificateurs de classe (override, default, delete, final)

Le spécificateur default permet de demander explicitement la génération automatique de la méthode correspondante. On l’utilise par exemple pour le constructeur de copie, le destructeur et l’opérateur de copie :

Inversement, le spécificateur delete interdira la génération automatique de la méthode correspondante. Utilisé pour un constructeur de copie et l’opérateur de copie, cela rend les objets de cette classe non copiable :

On obtient alors les erreurs suivantes :

error: use of deleted function ‘Point::Point(const Point&)'
     Point p3(p2);
note: declared here
       Point(const Point& point) = delete;

error: use of deleted function ‘Point& Point::operator=(const Point&)'
     p0 = p3;
note: declared here
       Point& operator=(const Point& point) = delete;

Dans la pratique :

  • Si un constructeur est déclaré explicitement, aucun constructeur par défaut n’est automatiquement généré.
  • Si un destructeur virtuel est déclaré explicitement, aucun destructeur par défaut n’est automatiquement généré.
  • Si un constructeur de déplacement ou un opérateur d’assignation de déplacement est déclaré explicitement :
    • Aucun constructeur de copie n’est généré automatiquement.
    • Aucun opérateur d’assignation de copie n’est généré automatiquement.
  • Si un constructeur de copie, un opérateur d’assignation de copie, un constructeur de déplacement, un opérateur d’assignation de mouvement ou un destructeur est déclaré explicitement :
    • Aucun constructeur de déplacement n’est généré automatiquement.
    • Aucun opérateur d’assignation de déplacement n’est généré automatiquement.

De plus, la norme C++11 spécifie les règles supplémentaires suivantes :

  • Si un constructeur de copie ou un destructeur est déclaré explicitement, la génération automatique de l’opérateur d’assignation de copie est déconseillée.
  • Si un opérateur d’assignation de copie ou un destructeur est déclaré explicitement, la génération automatique du constructeur de copie est déconseillée.

Dans une déclaration ou une définition de méthode, le spécificateur override garantit que la fonction membre est virtuelle et remplace une méthode virtuelle d’une classe de base.

Remarque : override permet d’énoncer que l’on fait une redéfinition et le compilateur en assurera le contrôle ! Il est donc fortement conseillé d’utiliser systèmatiquement override.

Inversement, le spécificateur final garantit que la méthode est virtuelle et spécifie qu’elle ne peut pas être remplacée par des classes dérivées. Lorsqu’il est utilisé dans une définition de classe, final spécifie que cette classe ne peut pas être dérivée.

Remarque : final permet de se protéger d’un remplacement non désiré et le compilateur en assurera le contrôle ! Il est donc fortement conseillé d’utiliser systèmatiquement final.

Référence sur rvalue

Chaque expression C++ a un type et appartient à une catégorie de valeur (lvalue, rvalue, …). Pour rappel, une lvalue (left value ou valeur à gauche) peut apparaître à gauche d’un opérateur d’affectation (un nom de variable par exemple). Une rvalue (right value ou valeur à droite) peut apparaître à droite d’un opérateur d’affectation (une expression par exemple). Maintenant, une rvalue peut être une prvalue (pure value) ou xvalue (eXpiring value).

Une prvalue est une expression dont l’évaluation :

  • calcule une valeur qui n’est pas associée à un objet
  • ou crée un objet temporaire et le désigne

Une xvalue est une glvalue qui désigne un objet dont les ressources peuvent être réutilisées. Une glvalue (generalized lvalue) est une expression dont l’évaluation détermine l’identité d’un objet.

Lien : value_category

Il est possible de créer des références sur des lvalue (avec l’opérateur « & ») et en C++11 sur des rvalue (avec l’opérateur « && ») :

On obtient :

foo(int x) -> 2
foo(int&& x) -> 42

Les références sur rvalue prennent en charge l’implémentation de la notion de déplacement (ce qui améliorera les performances en évitant des copies inutiles). La notion de déplacement est l’idée de transfèrer les ressources (telles que la mémoire allouée de manière dynamique) d’un objet vers un autre sans avoir à le copier.

La fonction move()

La fonction std::move() retourne une référence rvalue sur l’objet passé en argument. Il s’agit d’une fonction de service pour utiliser la notion (ou sémantique) de déplacement. La notion de déplacement est l’idée de transfèrer les ressources (telles que la mémoire allouée de manière dynamique) d’un objet vers un autre sans avoir à le copier.

Dans la bibliothèque standard, le déplacement implique que l’objet déplacé est laissé dans un état valide mais non spécifié. Ce qui signifie qu’après une telle opération, la valeur de l’objet déplacé ne doit être que détruite ou affectée d’une nouvelle valeur; y accéder donnera sinon une valeur non spécifiée.

Donc : dans une opération de déplacement (move), l’état de l’objet déplacé devient non défini. Cet objet ne doit plus être utilisé. Qu’est-ce que cela veut dire ? Si on déplace un objet p1 dans un objet p2, l’état de p1 n’est plus disponible car il n’est plus défini. Il ne faut donc plus utiliser p1 (mais p1 reste un objet valide). Seul l’objet p2 est viable.

Lien : move

Exemples :

Il est possible de “convertir” une lvalue en référence rvalue en utilisant donc la fonction std::move() ou static_cast :

On obtient :

foo(int&& x) -> 2
foo(int&& x) -> 2

Déplacement (constructeur et opérateur)

Le C++11 introduit un nouveau constructeur : le constructeur de déplacement. Sa signature sera : T(T&& t). Son objectif est de “voler” les ressources de l’objet passé en paramètre tout en le laissant dans un état valide mais non spécifié (cet objet passé en paramètre pourra par la suite être détruit ou recevoir une nouvelle valeur). L’objectif du constructeur de déplacement est donc d’éviter des copies inutiles et par conséquence d’améliorer les performances du programme.

Exemples d’appel de constructeurs :

Le C++11 introduit un nouvel opérateur d’affectation : l’opérateur de déplacement. Sa signature sera : T& operator=(T&& t). Son objectif est de “voler” les ressources de l’objet passé en paramètre tout en le laissant dans un état valide mais non spécifié (cet objet passé en paramètre pourra par la suite être détruit ou recevoir une nouvelle valeur). L’objectif de l’opérateur de déplacement est donc d’éviter des copies inutiles et par conséquence d’améliorer les performances du programme.

Exemples d’appel de l’opérateur d’affectation = :

Le constructeur de déplacement et l’opérateur de déplacement utilisent les références sur rvalue. On peut leur ajouter le qualificateur noexcept s’ils ne lancent pas d’exception.

Liens :

Exemple pour une classe Point :

struct Coordonnee
{
    double x;
    double y;
    Coordonnee() : x(0.), y(0.) {}
    Coordonnee(double x, double y) : x(x), y(y) {}
};

class Point
{
   private:
      Coordonnee *coordonnee;
      
   public:
      // Constructeurs
      Point() : coordonnee(new Coordonnee()) { }
      Point(double x, double y) : coordonnee(new Coordonnee(x, y)) { }
      Point(const Point& point); // copie
      Point(Point&& point) noexcept; // déplacement
      
      // Destructeur
      ~Point() { if(coordonnee) delete coordonnee; };

      // Accesseurs et mutateurs
      double getX() const { return coordonnee->x; }
      void setX(double x) { this->coordonnee->x = x; }
      double getY() const { return coordonnee->y; }
      void setY(double y) { this->coordonnee->y = y; }

      // Surcharge
      Point& operator=(const Point& point); // copie
      Point& operator=(Point&& point); // déplacement
      friend ostream& operator<<(ostream& os, const Point& point);
      friend Point operator+(const Point& p1, const Point& p2);

      // Services (exemples)
      static void swap_v1(Point& a, Point& b);
      static void swap_v2(Point& a, Point& b);
};

// Constructeur de copie
Point::Point(const Point& point) : coordonnee(new Coordonnee(point.coordonnee->x, point.coordonnee->y))
{
}

// Constructeur de déplacement (le "vol")
Point::Point(Point&& point) noexcept : coordonnee(point.coordonnee)
{
    point.coordonnee = nullptr;
}

// Copie
Point& Point::operator=(const Point& point)
{
    if(this != &point)
    {
        delete coordonnee;
        coordonnee = new Coordonnee(point.coordonnee->x, point.coordonnee->y);
    }
    return *this;
}

// Déplacement
Point& Point::operator=(Point&& point)
{
    if(this != &point)
    {
        delete coordonnee;
        coordonnee = point.coordonnee; // "vol"
        point.coordonnee = nullptr; // valide mais non spécifié
    }
    return *this;
}

// Surcharge
ostream& operator<<(ostream& os, const Point& point)
{
   os << "<" << point.coordonnee->x << "," << point.coordonnee->y << ">";
   return os;
}

Point operator+(const Point& p1, const Point& p2) 
{
   Point p;
   p.coordonnee->x = p1.coordonnee->x + p2.coordonnee->x;
   p.coordonnee->y = p1.coordonnee->y + p2.coordonnee->y;
   return p;
}

void Point::swap_v1(Point& a, Point& b) // par copie
{
    Point tmp(a);   // constructeur de copie
    a = b;          // opérateur de copie
    b = tmp;        // opérateur de copie
}

void Point::swap_v2(Point& a, Point& b) // par déplacement
{
    Point tmp(move(a));
    a = move(b);
    b = move(tmp);
}

Exemple n°1 : le déplacement en action

On obtient :

points :
default Point 0x7ffc389b1908
Point 0x7ffc389b1910
Point 0x7ffc389b1918
p2 = <0,0>
p3 = <1,1>
p4 = <2.5,2.5>

p2 = p3 + p4
default Point 0x7ffc389b1920
move operator= 0x7ffc389b1908
p2 = <3.5,3.5>

p5 <- p2
move Point 0x7ffc389b1920
p5 = <3.5,3.5>

p3 <-> p4
copy Point 0x7ffc389b18c0
copy operator= 0x7ffc389b1910
copy operator= 0x7ffc389b1918
p3 = <2.5,2.5>
p4 = <1,1>

p3 <-> p4
move Point 0x7ffc389b18c0
move operator= 0x7ffc389b1910
move operator= 0x7ffc389b1918
p3 = <1,1>
p4 = <2.5,2.5>

Exemple n°2 : amélioration des performances

On obtient :

  • par copie :
Duration : 0.193734 s
Constructions : 4548575
Copies : 1000000
Deplacements : 0
Total : 4548575
  • par déplacement :
Duration : 0.0961808 s
Constructions : 1000000
Copies : 0
Deplacements : 3548575
Total : 4548575

Threads

C++11 fournit une classe pour représenter les threads d’exécution individuels.

Un thread est un fil d’exécution (une séquence d’instructions) qui peut être exécuté simultanément avec d’autres fils de ce type dans des environnements multithreading, tout en partageant un même espace d’adressage.

Un objet thread initialisé représente un thread d’exécution actif. Un tel objet thread est joignable et possède un identifiant de thread unique.

Lien : thread

Exemple avec un thread :

Exemple avec deux threads :

Voir aussi :

std::future et std::async

std::future est un objet qui peut récupérer une valeur de manière synchronisée. std::asyncpermet d’appeler une fonction de manière asynchrone (sans attendre la fin de l’exécution de la fonction). La valeur retournée par la fonction sera accessible via l’objet future retourné lors de l’appel et en appelant sa méthode get().

Liens :

Mutex

Un mutex est un objet verrouillable conçu pour protéger les accès aux sections critiques de code en empêchant d’autres threads de s’exécuter simultanément et d’accéder aux mêmes emplacements mémoire.

Lien : mutex

Voir aussi : lock_guard

lock_guard est un objet qui gère un mutex en le gardant toujours verrouillé.

Voir aussi : condition_variable

std::ref

La fonction std::ref (dans <functional>) retourne un objet de type std::reference_wrapper<T> qui est en fait une référence sur l’élément.

Lien : std::ref

Les tableaux à taille fixe array

C++11 fournit le nouveau type de tableau std::array en tant que conteneur standard (défini dans l’en-tête <array>). Contrairement aux autres conteneurs standards, les tableaux array ont une taille fixe.

array fonctionne de la même manière que les tableaux en C sauf qu’il permet d’être copié (opération relativement coûteuse car c’est une copie de la totalité du bloc de mémoire) et peut s’utiliser explicitement en pointeur.

Lien : array

#include <iostream>
#include <array>

#define TAILLE  3

using namespace std;

int main()
{
    // En C/C++
    cout << "-> En C/C++" << endl;
    
    int t1[TAILLE] = {10, 20, 30};

    cout << "Elements du tableau t1 (avant) : " << endl;
    for (int i=0; i<TAILLE; ++i)
        cout << t1[i] << " ";
    cout << endl;
    
    for (int i=0; i<TAILLE; ++i)
        ++t1[i];

    cout << "Elements du tableau t1 (après) : " << endl;
    for (int i=0; i<TAILLE; ++i)
        cout << t1[i] << " ";
    cout << endl;

    cout << "Elements du tableau t1 (après) : " << endl;
    for (int element : t1)
        cout << element << " ";
    cout << endl;

    cout << endl;

    // En C++11
    cout << "-> En C++11" << endl;
    
    array<int,TAILLE> t2 {10, 20, 30};

    cout << "Elements du tableau t2 (avant) : " << endl;
    for (int i=0; i<t2.size(); ++i)
        cout << t2[i] << " ";
    cout << endl;

    for (int i=0; i<t2.size(); ++i)
        ++t2[i];

    cout << "Elements du tableau t2 (après) : " << endl;
    for (int i=0; i<t2.size(); ++i)
        cout << t2[i] << " ";
    cout << endl;

    cout << "Elements du tableau t2 (après) : " << endl;
    for (int element : t2)
        cout << element << " ";
    cout << endl;

    // Avec un pointeur
    int *t3 = t2.data(); // data() renvoie un pointeur vers le premier élément du tableau

    // Dans array, les éléments du tableau sont stockés dans des emplacements mémoire contigus,
    // le pointeur récupéré (ici t3) peut être utilisé pour accéder à n'importe quel élément du tableau
    cout << "Elements du tableau t2 (avec un pointeur) : " << endl;
    for (int i=0; i<TAILLE; ++i)
        cout << t3[i] << " "; // cout << *(t3+i) << " ";
    cout << endl;

    // Avec un itérateur
    cout << "Elements du tableau t2 (avec un itérateur) : " << endl;
    //for(array<int,TAILLE>::iterator it = t2.begin(); it != t2.end(); ++it)
    for(auto it = t2.begin(); it != t2.end(); ++it)
        cout << *it << " ";
    cout << endl;

    return 0;
}

Les listes simplement chaînée

forward_list est l’implémentation d’une liste simplement chaînée accessible seulement par sa tête (front).

Lien : http://www.cplusplus.com/reference/forward_list/forward_list/[forward_list]

Le type Tuple

Un tuple est une collection de dimension fixe d’objets de types différents. Tout type d’objet peut être élément d’un tuple. Cette nouvelle fonctionnalité est implémentée dans le nouvel en-tête <tuple> et bénéficie des extensions de C++11.

Lien : tuple

Tables de hachage

Une table de hachage (hash table) est une structure de données qui permet une association clé-élément. Il s’agit d’un tableau ne comportant pas d’ordre (contrairement à un tableau ordinaire qui est indexé par des entiers). On accède à chaque élément de la table par sa clé. L’accès s’effectue par une fonction de hachage qui transforme une clé en une valeur de hachage (un nombre) indexant les éléments de la table.

Pour éviter les conflits de noms avec les bibliothèques non standards qui ont leur propre implémentation des tables de hachage, on utilisera le préfixe unordered au lieu de hash.

Il existe deux types de tables de hachage dans la STL :

  • hash_set<K> : table de hachage simple, stocke seulement des clés de type K.
  • hash_map<K,T> : table de hachage double, stocke des clés de type K associées à des valeurs de type T. À une clé donnée ne peut être stockée qu’une seule valeur.

Liens :

Remarque : hash_set et hash_map font partie de la STL mais ne sont pas intégrés à la bibliothèque standard C++. Les compilateurs GNU C++ et Visual C++ de Microsoft les ont quand même implémentés.

Le standard C++11 propose des conteneurs similaires : unordered_set et unordered_map.

Liens :

On peut créer sa propre fonction de hachage avec un foncteur (Function Object) est un objet qui se comporte comme une fonction en surchargeant l’opérateur () :

On peut utiliser unordered_map avec ses propres classes à condition de définir l’opérateur == :

Nombres pseudo-aléatoires

La bibliothèque standard du C permet de générer des nombres pseudo-aléatoires grâce à la fonction rand().

C++11 va fournir une manière différente de générer les nombres pseudo-aléatoires :

  • un moteur de génération, qui contient l’état du générateur et produit les nombres pseudo-aléatoires ;
  • une distribution, qui détermine les valeurs que le résultat peut prendre ainsi que sa loi de probabilité.

C++11 définit trois algorithmes de génération (linear_congruential, subtract with carry et mersenne_twister), chacun ayant des avantages et des inconvénients et fournira un certain nombre de lois standard (uniform_int_distribution, bernoulli_distribution, …).

Lien : random

Fonction lambda

Une lambda est une fonction possiblement anonyme et destinée à être utilisée localement.

Liens :

Syntaxe :

[zone de capture](paramètres de la lambda) -> type de retour { instructions }

Exemple simpliste :

Exemples basiques :

Les expressions Lambda (ou closure) sont donc un bon moyen de passer du code en paramètre d’une fonction :

Lien : count_if

Remarque : Cette utilisation est simple et place le code du prédicat au bon endroit (sans avoir besoin de déclarer une fonction pour cela).

Les lambdas peuvent accéder aux variables dans la portée par référence ou par valeur :

std::function et std::mem_fn

Le template std::function permet d’encapsuler un pointeur de fonction ou une lambda :

Le template std::mem_fn permet d’encapsuler un pointeur de méthode (fonction membre) d’une classe :

Liens :

C++14

Lien : C++14

Nombres binaires

Avec le C++14, il est désormais possible de spécifier des nombres binaires en utilisant le préfixe 0b ou 0B :

Séparateur de chiffres

Pour améliorer la lisibilité :

C++17

Liens :

Le type byte

En C++17, std::byte (dans <cstddef>) représente un octet en mémoire. En C/C++, on utilisait le type char ou unsigned char. Attention, std::byte n’est pas un type caractère et ni un type arithmétique. Seuls opérateurs au niveau du bit ont été surchargés :

  • les opérateurs de décalage comme <<, >>, <<=, >>=
  • les opérateurs logiques comme |, &, ^, ~, |=, &=, ^=

Le type byte n’est pas directement utilisable comme un entier sauf via la fonction std::to_integer<>().

Lien : std::byte

std::invoke

En C++17, la fonction std::invoke (dans <functional>) permet d’appeler une fonction ou une méthode en lui passant des arguments.

Lien : std::invoke

std::optional

En C++17, le type std::optional<T> peut contenir une valeur ou pas. On utilise la méthode has_value() pour déterminer s’il y a une valeur dans l’objet et value() pour la récupérer.

Lien : std::optional

std::any

En C++17, le type std::any peut contenir n’importe quel type ou aucune valeur. C’est l’équivalent d’un void * type-safe. On peut utiliser la méthode has_value() pour déterminer s’il y a une valeur. Il existe aussi any_cast<T>() pour réaliser des conversions vers des types T. type() retourne une référence sur le type type_info.

Lien : std::any

C++20

Liens :

Wikipédia

Voir aussi