Ce cours est visible gratuitement en ligne.

Got it!
Introduction à C++ 2011 (C++0x)
Last updated on Tuesday, November 19, 2013
  • 4 heures
  • Moyen

Introduction du cours

Bonjour à tous !
Vous avez entendu parler de la nouvelle norme C++, ces derniers temps ?
C++0x et C++1x vous disent-ils quelque chose ?
Vous voulez en apprendre plus sur cette norme ?
Alors, vous êtes au bon endroit !

Ce tutoriel a pour but de vous montrer quelques nouveautés de la norme C++ 2011 qui sont déjà utilisables par les compilateurs actuels.

Pourquoi une nouvelle révision du langage C++ ?

La norme actuelle du C++ date de 1998, puis a été mise à jour en 2003.
Nous pouvons nous demander pourquoi faire une nouvelle norme maintenant.

Pour répondre à la question, je cite Wikipédia :

Citation : Wikipédia

Un langage de programmation comme le C++ suit une évolution qui permettra aux programmeurs de coder plus rapidement, de façon plus élégante et permettant de faire du code maintenable.

Certaines nouveautés permettent en effet d’écrire du code C++ plus simple, comme vous le verrez dans quelques instants.

En outre, le matériel informatique évoluant constamment, en particulier les processeurs, les besoins sont maintenant différents en matière de programmation.
Ayant aujourd’hui plusieurs cœurs, les processeurs peuvent exécuter plusieurs instructions en même temps.
Des classes permettant l’utilisation de la programmation concurrente sont ainsi apparues dans cette norme du C++.

Est-ce que la nouvelle norme du C++ est rétro-compatible avec l’ancienne ?

Presque.
La plupart du code déjà écrit n’aura donc pas besoin d’être modifié.

Préparation du compilateur pour utiliser C++ 2011

Je vais vous montrer comment configurer g++ 4.6 sur Ubuntu pour utiliser les nouveaux standards du C++.

Installation

Premièrement, ajoutez ce ppa à vos sources :

deb http://ppa.launchpad.net/ubuntu-toolchain-r/test/ubuntu YOUR_UBUNTU_VERSION_HERE main

en modifiant YOUR_UBUNTU_VERSION HERE par votre version d’Ubuntu (natty, maverick, lucid ou karmic).

Vous pouvez maintenant mettre à jour le compilateur g++ :

sudo apt-get install g++

Après l’installation, tapez la commande suivante pour vérifier que vous avez au moins la version 4.6 de g++ :

g++ -v

La dernière ligne de la sortie devrait vous le confirmer :

gcc version 4.6.1 20110409 (prerelease) (Ubuntu 4.6.0-3~ppa1)

Configuration de la compilation

Maintenant, vous allez essayer de compiler ce programme :

#include <iostream>

int main() {
    int tableau[5] = {1, 2, 3, 4, 5};
    for(int &x : tableau) {
        std::cout << x << std::endl;
    }
    return 0;
}

en utilisant la ligne de commande habituelle :

g++ main.cpp -o BoucleFor

J’ai la bonne version de g++, mais j’obtiens tout de même une erreur du compilateur.
Que dois-je faire ?

Vous devez simplement lui demander poliment de compiler en utilisant la nouvelle norme.
Sinon, vous pouvez avoir une erreur de ce type :

main.cpp: In function ‘int main()’:
main.cpp:5:18: error: range-based-for loops are not allowed in C++98 mode

Parfois, vous pouvez avoir un avertissement du genre :

main.cpp:3:1: warning: scoped enums only available with -std=c++0x or -std=gnu++0x [enabled by default]

qui vous indique un peu mieux que faire.

Lancez donc la dernière commande avec un nouvel argument :

g++ -std=c++0x main.cpp -o BoucleFor

Et voilà, ça compile !

Le précédent code source vous met-il l’eau à la bouche ?
Si c’est la cas, alors continuez votre lecture, vous n’avez pas terminé de découvrir de nouvelles façons d’écrire du code plus simplement qu’avant !

Les énumérations fortement typées

Pour commencer, voyons quelque chose de facile : les énumérations fortement typées.

Les énumérations

Vous connaissez déjà les énumérations, que l’on peut créer comme ceci :

enum Direction { HAUT, DROITE, BAS, GAUCHE };

On peut ensuite créer une variable utilisant cette énumération :

Direction direction = HAUT;

Puis, nous pouvons afficher cette variable :

std::cout << direction << std::endl;

Cela fonctionne, car il y a une conversion implicite vers un entier.
C’est comme si nous avions fait ceci :

std::cout << static_cast<int>(direction) << std::endl;

Les énumérations fortement typées

Passons aux nouveautés.
Les énumérations fortement typées se créent en ajoutant le mot-clé class après enum. :

enum class Direction { Haut, Droite, Bas, Gauche };

Par la suite, pour l’utiliser, il faut utiliser le nom de l’énumération, suivi par l’opérateur de résolution de portée et du nom de la valeur que l’on souhaite utilisée :

Direction direction = Direction::Haut;

C’est la première différence avec les énumérations que vous connaissez.

La seconde, c’est qu’il n’y a pas de conversion implicite vers un entier.
Ainsi, le code suivant ne compilera pas :

std::cout << direction << std::endl;

Pour qu’il compile, il faut explicitement convertir la variable en entier :

std::cout << static_cast<int>(direction) << std::endl;

Mais, à quoi ça sert d’utiliser ces énumérations ?
C’était bien plus simple avant, non ?

Ces énumérations n’ont pas été créées pour le simple plaisir de compliquer les choses.
Elles permettent d’assurer une certaine sécurité en codant.

En effet, vous ne voulez sûrement pas faire des opérations mathématiques avec votre énumération.
Le code suivant n’aurait aucun sens :

int nombre = 5 + Direction::Droite;

Ce code provoque une erreur de compilation et c’est bien, car ça n’a pas de sens d’ajouter une direction à un nombre.

Le « bug » des chevrons

Vous souvenez-vous de la façon de créer un vecteur de vecteurs ?
Il y a un petit point à ne pas oublier ; il faut mettre un espace entre les deux derniers chevrons :

std::vector<std::vector<int> > nombres;

Sinon, >> pourrait être confondu avec l’opérateur de flux.

Eh bien, savez-vous quoi ?
Il est maintenant possible d’écrire ceci en C++0x :

std::vector<std::vector<int>> nombres;

Il n’y a plus d’erreur de compilation.

Les listes d’initialisateurs

Côté utilisateur

Pour créer un vecteur avec différentes valeurs initiales, il faut écrire plusieurs lignes de code :

std::vector<int> nombres;
nombres.push_back(1);
nombres.push_back(2);
nombres.push_back(3);
nombres.push_back(4);
nombres.push_back(5);

Il est maintenant possible de faire beaucoup plus court avec une liste d’initialisateurs :

std::vector<int> nombres = { 1, 2, 3, 4, 5 };

C’est comme si on initialisait un tableau normal :

int nombres2[] = { 1, 2, 3, 4, 5 };

sauf qu’on peut le faire avec les conteneurs de la STL.

Voici un code complet présentant comment utiliser une liste d’initialisateurs avec un std::map :

#include <iostream>
#include <map>
#include <ostream>
#include <string>

int main() {
    std::map<int, std::string> nombres = { { 1, "un" }, { 2, "deux" }, { 3, "trois" } };
    std::cout << nombres[1] << std::endl;
}

Simple, non ?

Côté créateur

Vous avez utilisé les listes d’initialisateurs, mais aimeriez-vous créer des fonctions qui les utilisent ?
C’est ce que nous allons voir ici.

Nous allons créer une fonction très banale qui ne fait qu’afficher les éléments passés à la fonction à l’aide de la liste d’initialisateurs.

Notre fonction main() sera :

int main() {
    afficherListe({ 1, 2, 3, 4, 5 });
    return 0;
}

Nous souhaitons implémenter la fonction afficherListe() qui affichera les éléments envoyés.

La fonction aura l’allure suivante :

void afficherListe(std::initializer_list<int> liste) {
    //Affichage de la liste.
}

Étant donné que nous avons indiqué int entre les chevrons, nous acceptons seulement les entiers dans la liste.

Le code de la fonction est donc :

void afficherListe(std::initializer_list<int> liste) {
    for(std::initializer_list<int>::iterator i(liste.begin()) ; i != liste.end() ; ++i) {
        std::cout << *i << std::endl;
    }
}

N’oubliez pas d’inclure l’entête :

#include <initializer_list>

Le mot-clé auto

Le mot-clé auto est un mot-clé utilisable en C++0x qui est différent du mot-clé auto qui était utilisé avant. Il est possible de le mettre à la place du type de telle sorte que ce type est automagiquement déterminé à la compilation en fonction du type retourné par l’objet utilisé pour l’initialisation. :magicien:
On parle ici d’inférence de types.

Donc, il est possible d’écrire le code suivant :

auto nombre = 5;

La variable nombre sera de type entier (int).
Mais, je vous interdis de faire cela ! :colere:
Si vous initialisez toutes vos variables avec le mot-clé auto, votre code va vite devenir illisible.

Vous devez utiliser ce mot-clé le moins possible.

As-tu des exemples d’utilisation ?

Oui, en voilà un :

for(auto i(nombres.begin()) ; i != nombres.end() ; ++i) {
    std::cout << *i << std::endl;
}

Au lieu de cela :

for(std::vector<int>::iterator i(nombres.begin()) ; i != nombres.end() ; ++i) {
    std::cout << *i << std::endl;
}

Mais, encore là, c’est limite.

Si vous utilisez beaucoup de fois un même itérateur, le mieux est de créer un typedef :

typedef std::vector<int>::iterator iter_entier;
for(iter_entier i(nombres.begin()) ; i != nombres.end() ; ++i) {
    std::cout << *i << std::endl;
}

Vous pouvez également utiliser ce mot-clé lorsqu’une même fonction peut retourner différents types de données.

Si je déclare une variable avec le mot-clé auto, puis-je donner le même type à une autre variable ?

Oui, avec le mot-clé decltype :
Ce mot-clé détermine le type d’une expression.

auto variable = 5;
decltype(variable) autreVariable;

Ainsi, nous sommes sûr que autreVariable aura le même type (int) que variable.

L’inférence de type est très utile lorsque nous utilisons la programmation générique.
Considérez ce programme :

#include <iostream>

template <typename T>
T maximum(const T& a, const T& b) {
    if(a > b) {
        return a;
    }
    else {
        return b;
    }
}

template <typename T>
T minimum(const T& a, const T& b) {
    if(a < b) {
        return a;
    }
    else {
        return b;
    }
}

int main() {
    int a(10), b(20);
    auto plusGrand = maximum(a, b);
    decltype(plusGrand) plusPetit = minimum(a, b);
    std::cout << "Le plus grand est : " << plusGrand << std::endl;
    std::cout << "Le plus petit est : " << plusPetit << std::endl;
    return 0;
}

Ce code détermine, grâce à des fonctions génériques, le plus grand et le plus petit nombres.

Ce qui est intéressant, c’est que nous n’avons besoin que de modifier la première ligne de la fonction main() si nous voulons essayer ce code avec un autre type.
Remplacez-la par celle-ci et tout fonctionne :

double a(10.5), b(20.5);

De plus, les mots-clés auto et decltype sont obligatoires dans certains cas utilisant la programmation générique.

Considérons l’exemple suivant (assez tordu, je dois l’admettre :honte: ) :
Vous avez un programme de gestion des notes obtenues à l’école.
Les examens comportent dix questions (chacune vaut 10 points).
Votre programme stocke les notes de deux manières :

  • Un nombre entier sur 100 (le pourcentage) ;

  • Un nombre réel sur 1 (le pourcentage divisé par 100).

Vous avez créé une fonction ajouterDixPourcents() surchargée pour les entiers et les réels :

int ajouterDixPourcents(int nombre) {
    if(nombre + 10 <= 100) {
        return nombre + 10;
    }
    else {
        return 100;
    }
}

float ajouterDixPourcents(float nombre) {
    if(nombre + 0.1 <= 1) {
        return nombre + 0.1;
    }
    else {
        return 1;
    }
}

Pour une raison inconnue :lol: , vous créez également une fonction générique ajouter() qui appellera la fonction ajouterDixPourcents().
Cette fonction doit retourner le même type que celui retourné par cette dernière.
Mais, comment faire pour déterminer ce type ?

Nous pourrions utiliser le mot-clé auto, non ?

Impossible, car il se base sur le type de l’objet qu’on lui affecte. Le compilateur ne saura pas sur quel objet se baser.

Avec decltype(), alors ?

Encore une fois, c’est impossible.
En effet, quelle expression lui passerions-nous pour déterminer le type de retour de la fonction ajouter() ?

La solution est d’utiliser les deux mots-clés en utilisant une syntaxe alternative pour le prototype de la fonction ! (Vous ne pouviez pas vraiment le deviner. :-° )

Voilà enfin la fonction ajouter() :

template <typename T> auto ajouter(T nombre) -> decltype(ajouterDixPourcents(nombre)) {
    return ajouterDixPourcents(nombre);
}

Vous pouvez maintenant utiliser votre fonction générique ajouter() dans votre code :

int main() {
    int pourcentageEntier(42);
    float pourcentageFlottant = pourcentageEntier / 100.f;
    std::cout << pourcentageFlottant << " = " << pourcentageEntier << "%" << std::endl;
    
    pourcentageEntier = ajouter(pourcentageEntier);
    pourcentageFlottant = ajouter(pourcentageFlottant);
    std::cout << pourcentageFlottant << " = " << pourcentageEntier << "%" << std::endl;
}

Étant donnée que nous utilisons auto et decltype dans une fonction dont le prototype est plutôt long, je vous donne un exemple plus simple pour comprendre :

auto cinq() -> int {
    return 5;
}

Nous utilisons le mot-clé auto à la place d’indiquer un type de retour.
Il faut donc préciser le type de retour après la parenthèse fermante sous la forme suivante :

-> type

type peut être un type prédéfini comme int, mais la plupart du temps, nous utiliserons decltype(expression).

La boucle basée sur un intervalle

Vous vous rappelez du code montré dans la deuxième partie de ce tutoriel ?
Cette boucle ressemblant au foreach de nombreux autres langages ?

Eh bien, il est maintenant possible d’utiliser une telle boucle en C++ !

Alors qu’autrefois nous étions obligés d’utiliser l’algorithme for_each :

#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;

void afficherElement(int element) {
    cout << element << endl;
}

int main() {
    vector<int> nombres = { 1, 2, 3, 4, 5 };
    for_each(nombres.begin(), nombres.end(), afficherElement);
    
    return 0;
}

Nous pouvons maintenant utiliser la syntaxe beaucoup plus simple de la boucle for :

#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> nombres = { 1, 2, 3, 4, 5 };
    for(int &element : nombres) {
        cout << element << endl;
    }
    
    return 0;
}

Comment cela fonctionne-t-il ?

Prenons la ligne du for :

for(int &element : nombres)

Sur cette ligne, nous trouvons d’abord la déclaration de la variable qui prendra chacune des valeurs du vecteur situé après les deux-points (:).
Donc, au premier tour de boucle, element vaudra 1.
Au deuxième tour, element aura pour valeur 2.
Et ainsi de suite.

Étant donné que nous utilisons une référence sur l’élément, nous pouvons le modifier sans problème :

for(int &element : nombres) {
    ++element;
}

Pour parcourir un std::map, le principe est le même :

map<int, string> nombres = { { 1, "un" }, { 2, "deux" }, { 3, "trois" } };
for(auto i : nombres) {
    cout << i.first << " => " << i.second << endl;
}

Initialisation d’un pointeur

En C, vous aviez l’habitude d’initialiser un pointeur avec la macro NULL :

int *pointeur = NULL;

Mais, nous évitons ceci en C++, car nous détestons utiliser des macros.

Vous avez donc pris l’habitude d’initialiser vos pointeurs à 0 :

int *pointeur(0);

Ça fonctionne toujours, mais sachez que vous pouvez maintenant initialiser vos pointeurs avec le nouveau mot-clé nullptr :

int *pointeur(nullptr);

Maintenant, impossible de ne pas voir du premier coup d’œil qu’il s’agit d’un pointeur.

Enfin, sachez qu’il est impossible d’utiliser nullptr comme un entier, ce qui était possible avec NULL.
En effet, ce code compile :

int nul = NULL;

Mais, pas celui-ci :

int nul = nullptr;

Quelle en est la raison ?

Parce que nullptr est de type std::nullptr_t (un peu comme true et false sont de type bool), alors que NULL est une macro qui vaut 0.

Les fonctions anonymes et les fermetures

Fonctions anonymes

Une fonction anonyme, communément appelée fonction lambda, est une fonction qui n’a pas … de nom.
C’est aussi simple que cela.
On peut envoyer de telles fonctions en paramètre à des fonctions ou bien les stocker dans une variable.

Par exemple, au lieu d’envoyer un foncteur à std::for_each, nous pouvons lui envoyer une fonction anonyme.
C’est pratique dans le cas où nous ne comptons pas réutiliser la fonction ailleurs.
Reprenons l’exemple donné plus haut et utilisons à la place une fonction anonyme :

#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;

int main() {
    vector<int> nombres = { 1, 2, 3, 4, 5 };
    for_each(nombres.begin(), nombres.end(), [](int element) {
        cout << element << endl;
    });
    
    return 0;
}

Nous allons étudier la partie qui doit vous sembler étrange :

[](int element) {
    cout << element << endl;
}

Premièrement, il y a des crochets ; nous verrons plus tard à quoi ils servent.

Ensuite, il y a directement les paramètres de la fonction, sans le nom devant.
C’est tout à fait normal, les fonctions anonymes n’ont pas de nom.

Le reste est comme une fonction ordinaire : il y a l’accolade ouvrante, les instructions et l’accolade fermante.

Vous pouvez assigner une fonction anonyme à une variable, comme ceci :

std::function<int (int, int)> addition = [](int x, int y) -> int {
    return x + y;
};

Premièrement, il y a le type std::function<> que nous devons assigner à la variable.
Entre les chevrons, nous devons mettre le type de retour, suivi du type des paramètres, entre parenthèses, de la fonction anonyme.
En pratique, nous donnerons auto comme type à une variable dans laquelle nous voulons mettre une fonction anonyme :

auto addition = [](int x, int y) -> int {
    return x + y;
};

Dans cet exemple, il y a une petite nouveauté :

[](int x, int y) -> int {
    return x + y;
}

Après la parenthèse fermante, il y a -> int.
Ce code est facultatif et il indique le type de retour de la fonction anonyme.
Cela peut être utile de le mettre dans le cas où nous donnons le type auto à la variable ou lorsque nous envoyons une fonction anonyme à une autre fonction.
Ainsi, nous saurons rapidement quel type de variable retourne la fonction anonyme.

Si nous ne le mettons pas, le type sera calculé par decltype().

Fermetures

Une fermeture est une fonction qui capture des variables à l’extérieur de celle-ci.
Il faut préciser quelles sont les variables capturées.

Nous devons le faire entre les crochets ([]) qui se trouvent au début d’une fonction anonyme.
Pour capturer un variable, il faut écrire sont nom entre crochet.

Sachez qu’il y a deux façons de capturer une variable :

  • par référence : de cette façon, les modifications apportées à la variable s’appliqueront à la variable capturée ;

  • par valeur : c’est une copie de la variable capturée ; les modifications n’affecteront pas cette dernière.

Pour capturer une variable par référence, il suffit de précéder son nom par l’esperluette (&), comme ceci :

[&somme](std::vector<int> vecteur {
    //...
}

Si nous voulons capturer une variable par valeur, il suffit d’écrire son nom :

[x](int &a) {
    a = x;
}

Voyons un exemple complet :

int somme(0);
std::function<void (std::vector<int>)> sommeVecteur = [&somme](std::vector<int> vecteur) {
    for(int &x : vecteur) {
        somme += x;
    }
};

std::vector<int> nombres = { 1, 2, 3, 4, 5 };

sommeVecteur(nombres);
std::cout << somme << std::endl;

Nous avons la confirmation que la variable somme a bien été modifiée : le nombre 15 s’affiche.

Il est possible de capturer toutes les variables par référence ou par valeur.

Voici une liste pour en résumer la syntaxe :

  • [] : pas de variable externe ;

  • [x, &y] : x capturée par valeur, y capturée par référence ;

  • [&] : toutes les variables externes sont capturées par référence ;

  • [=] : toutes les variables externes sont capturées par valeur ;

  • [&, x] : x capturée par valeur, toutes les autres variables, par référence ;

  • [=, &x] : x capturée par référence, toutes les autres variables, par valeur.

Bon, vous êtes prévenus, mais voici tout de même un exemple :

int x(10);
auto afficherVariableX = [x]() {
    std::cout << x << std::endl;
};

afficherVariableX();

Comme vous pouvez le constater, la variable externe x a été affichée.

Les tableaux à taille fixe

Vous vous souvenez des tableaux statiques du Cours C++ ?
Ceux que nous pouvons déclarer comme suit :

int tableauFixe[5];

Eh bien, il y a maintenant un conteneur de la STL pour ce genre de tableau.

Mais pourquoi créer un conteneur pour une chose existant déjà dans le langage ?

Le but de ce conteneur est de pouvoir utiliser les tableaux statiques de la même manière que les autres conteneurs de la STL tout en offrant des performances semblables aux tableaux statiques que vous connaissez déjà.

Premièrement, vous devez inclure cet en-tête :

#include <array>

Déclaration et initialisation

Pour déclarer un tableau fixe de cinq entiers, procéder comme suit :

std::array<int, 5> tableauFixe;

Regardez ce schéma pour mieux comprendre :
std::array<TYPE, TAILLE> NOM;

Vous pouvez en même temps initialiser le tableau avec la liste d’initialisateurs que vous connaissez déjà bien :

std::array<int, 5> tableauFixe = { 1, 2, 3, 4, 5 };

La taille ne peut pas être une variable, mais peut être une constante.
Donc, ce code ne compilera pas :

int taille = 5;
std::array<int, taille> tableauFixe = { 1, 2, 3, 4, 5 };

Mais, ce code compilera :

int const taille = 5;
std::array<int, taille> tableauFixe = { 1, 2, 3, 4, 5 };

Il est ensuite possible de parcourir le tableau avec une boucle basée sur un intervalle :

for(int nombre : tableauFixe) {
    std::cout << nombre << std::endl;
}

Méthodes

Pour obtenir la taille d’un tableau, utiliser l’habituelle méthode size().

Il est toujours possible d’accéder aux éléments d’un tableau avec l’opérateur [] et la méthode at() :

tableauFixe[0] = 10;
tableauFixe.at(1) = 40;
std::cout << tableauFixe[1] << std::endl;
std::cout << tableauFixe.at(0) << std::endl;

Les méthodes front() et back() permettent respectivement d’obtenir le premier et le dernier élément du tableau.

La méthode empty() nous indique si le tableau est vide.

Il est possible de modifier la valeur de tous les éléments par une autre avec la méthode fill().

Enfin, il est également possible d’utiliser les itérateurs avec les tableaux statiques en utilisant les méthodes begin() et end().

Exemple

Voici un exemple de code utilisant ces méthodes :

#include <array>
#include <iostream>

int main() {
    std::array<int, 5> tableauFixe = { 1, 2, 3, 4, 5 }; //Création d’un tableau à taille fixe de cinq entiers.
    
    //Affichage de tous ses éléments.
    for(int nombre : tableauFixe) {
        std::cout << nombre << std::endl;
    }
    
    std::cout << "Taille : " << tableauFixe.size() << std::endl; //Affichage de sa taille.
    
    //Modification d’éléments.
    tableauFixe[0] = 10;
    tableauFixe.at(1) = 40;
    std::cout << tableauFixe[1] << std::endl; //Affichage d’un élément précis sans vérification de limite.
    std::cout << tableauFixe.at(0) << std::endl; //Même chose sauf qu’il y a une vérification de limite.
    std::cout << tableauFixe.front() << std::endl; //Afficher le premier élément.
    std::cout << tableauFixe.back() << std::endl; //Afficher le dernier élément.
    
    if(not tableauFixe.empty()) {
        std::cout << "Le tableau n’est pas vide." << std::endl;
    }
    
    tableauFixe.fill(5); //Modifier tous les éléments pour la valeur 5.
    
    for(std::array<int, 5>::iterator i(tableauFixe.begin()) ; i != tableauFixe.end() ; ++i) {
        std::cout << *i << std::endl;
    }
    return 0;
}

Un nouveau type de conteneur : le tuple

Commençons par définir ce qu’est un tuple :

Citation : Wikipédia

Un tuple est une collection de dimension fixe d'objets de types différents.

Un tuple permet donc de créer un tableau statique, comme nous venons de voir, mais avec des types d’objet différents.

L’en-tête à inclure est :

#include <tuple>

Déclaration et initialisation

Voyons un exemple de déclaration d’un tuple :

std::tuple<int, double, std::string> nomTuple;

Ici, nous créons un tuple nommé nomTuple pouvant contenir un entier, un nombre réel et une chaîne de caractères.

La forme générale pour déclarer un tuple est :
std::tuple<type1, type2, …> nomTuple;

Si nous voulons initialiser le tuple lors de l’initialisation, nous devons envoyer des arguments au constructeur.

Imaginons un tuple qui contient des informations sur une personne.
Nous pourrions retenir son âge, sa taille et son prénom.

Il serait possible de déclarer et d’initialiser ce tuple comme suit :

std::tuple<int, double, std::string> personne(22, 185.4, "Jack");

Accéder aux éléments du tuple

Pour accéder aux éléments d’un tuple, nous devons utiliser la fonction std::get().

Par exemple, si nous voulons modifier l’âge de notre bon ami Jack, nous devons faire ceci :

std::get<0>(personne) = 23;

Ensuite, nous pouvons afficher son âge :

std::cout << "Âge : " << std::get<0>(personne) << std::endl;

Exemple

Voici un exemple :

#include <iostream>
#include <string>
#include <tuple>

int main() {
    std::tuple<int, double, std::string> personne(22, 185.4, "Jack"); //Création d’un tuple.
    std::get<0>(personne) = 23; //Modification d’un objet d’un tuple.
    
    //Affichage des objets du tuple.
    std::cout << "Âge : " << std::get<0>(personne) << std::endl << "Taille : " << std::get<1>(personne) << std::endl << "Prénom : " << std::get<2>(personne) << std::endl;
    
    typedef std::tuple<int, double, std::string> tuple_personne;
    std::cout << "Taille du tuple : " << std::tuple_size<tuple_personne>::value << std::endl; //Affichage de la taille du tuple.
    tuple_personne jack(23, 185.4, "Jack");
    if(personne == jack) { //Comparaison de tuples.
        std::cout << "Les tuples sont identiques." << std::endl;
    }
    return 0;
}

Comme vous pouvez le constater, nous pouvons vérifier que les valeurs de deux tuples sont égales grâce à l’opérateur ==.

Gestion du temps

Il y a maintenant une interface pour gérer le temps en C++.
Voyons tout de suite comment elle fonctionne.

Avant tout, incluez l’en-tête :

#include <chrono>

Obtenir le temps actuel

Pour obtenir le temps actuel, ce qui est utile pour faire une action à chaque X secondes, nous devons appeler la fonction std::chrono::system_clock::now() :

std::chrono::time_point<std::chrono::system_clock> temps = std::chrono::system_clock::now();

Ou plus simplement :

auto temps = std::chrono::system_clock::now();

Pour faire un test, nous allons appeler deux fois cette fonction, avec un appel à usleep() entre chaque appel.
Ensuite, nous afficherons le temps pris par le programme.

#include <chrono>
#include <iostream>

int main() {
    auto temps1 = std::chrono::system_clock::now();
    usleep(100000);
    auto temps2 = std::chrono::system_clock::now();
    
    std::cout << (temps2 - temps1).count() << " microsecondes." << std::endl;
    
    return 0;
}

Le résultat suivant peut s’afficher à l’écran :

100090 microsecondes.

Et voilà, nous avons pu calculer le temps pris par la fonction usleep() pour mettre en pause le programme.

Autres unités

On peut également décider d’afficher le temps dans une autre unité, par exemple en nanosecondes :

std::chrono::nanoseconds nbrNanoSecondes = (temps2 - temps1);
std::cout << nbrNanoSecondes.count() << " nanosecondes." << std::endl;

Le résultat :

100090000 nanosecondes.

J’ai essayé d’obtenir le résultat en millisecondes et ça ne fonctionne pas.
Est-ce normal ?

Oui, car il y aurait une perte de précisions étant donné que le nombre retourné est un entier.
Pour obtenir tout de même le nombre de millisecondes, il faut faire ceci :

std::chrono::duration<double, std::milli> nbrMilliSecondes = (temps2 - temps1);
std::cout << nbrMilliSecondes.count() << " millisecondes." << std::endl;

Toutes les unités que vous avez utilisées jusqu’à présent était des typedefs sur la classe std::chrono::duration.
Par exemple, pour les nanosecondes, le typedef est :

typedef duration<int64_t, nano> 	nanoseconds;

Le problème avec les millisecondes dans notre exemple est que le type utilisé est un entier.

Or, le nombre de millisecondes doit être de type double, sinon il y aurait une perte de précision.
C’est pourquoi nous devons utiliser la classe std::chrono::duration.

Pour obtenir le nombre de secondes, nous pouvons faire ceci :

std::chrono::duration<double, std::ratio<1>> nbrSecondes = (temps2 - temps1);

Ou plus simplement :

std::chrono::duration<double> nbrSecondes = (temps2 - temps1);

Les initialisateurs d’attributs

Les initialisateurs d’attributs permettent… d’initialiser les attributs (ah oui ? ^^ ) d’une classe ou d’un autre type de donnée.

Pour ce faire, il suffit d’écrire la valeur des attributs, dans le même ordre qu’ils sont déclarés dans la classe, entre accolades, séparés par une virgule.

Nous allons utiliser la classe suivante (qui a deux attributs publics) pour nos premiers tests :

struct Paire {
    int nombre1;
    int nombre2;
};

Dans notre code, il est maintenant possible d’utiliser cette syntaxe pour initialiser les attributs :

int main() {
    Paire paire{ 5, 25 }; //Initialise nombre1 à 5 et nombre2 à 25.
    return 0;
}

Nous pouvons ensuite modifier les attributs de l’objet en utilisant la même syntaxe :

paire = { 3, 9 };

Il est également possible d’utiliser cette syntaxe pour retourner un objet :

Paire getPaire() {
    return { 10, 100 }; //Retourne une Paire dont nombre1 = 10 et nombre2 = 100.
}

Dans notre fonction main(), nous pouvons tester cette fonction :

paire = getPaire();

Mais on nous a dit qu’il faut éviter à tout prix les attributs publics.
Alors, nous n’utiliserons pas beaucoup cette syntaxe, non ?

Eh bien… vous pouvez même l’utiliser avec des attributs privés, par l’intermédiaire du constructeur.

En voici un exemple :

#include <iostream>

class Paire {
    public:
        Paire(int nombre1, int nombre2) : nombre1_(nombre1), nombre2_(nombre2) {}
        
    private:
        int nombre1_;
        int nombre2_;
};

Paire getPaire() {
    return { 10, 100 }; //Retourne une Paire dont nombre1_ = 10 et nombre2_ = 100.
}

int main() {
    Paire paire{ 5, 25 }; //Initialise nombre1_ à 5 et nombre2_ à 25.
    paire = { 3, 9 };
    paire = getPaire();
    return 0;
}

À chaque fois, le constructeur est utilisé pour initialiser les attributs.

Paire(std::initializer_list<int> liste) : nombre1_(*liste.begin()), nombre2_(*liste.end()) {
    std::cout << "Liste d’initialisateurs." << std::endl;
}
bool booleen{false};
int nombre{42};
int tableau[5] = { 1, 2, 3, 4, 5 };
std::vector<int> vecteurNombres{ 1, 2, 3 };
std::string chaine{"Hello World!"};
int* pointeurSurEntier{nullptr};

Que de nouveautés dans la nouvelle norme, n’est-ce pas ?
Pourtant, nous n’avons pas tout vu, loin de là. o_O
Pour en apprendre encore plus, voici quelques liens :

Et si vous voulez savoir si telle ou telle fonctionnalité est implémentée dans gcc, allez sur ce site.

Enfin, si vous voulez apprendre à utiliser les threads de la nouvelle norme (utilisable avec g++ 4.6), il y a une excellente série d’articles pour les utiliser en C++ sur ce site.

g++ -std=c++0x -pthread thread.cpp -o Thread

Merci à Babilomax pour ses commentaires et remarques très utiles.

How courses work

  • 1

    You have now access to the course contents and exercises.

  • 2

    You will advance in the course week by week. Each week, you will work on one part of the course.

  • !

    Exercises must be completed within one week. The completion deadline will be announced at the start of each new part in the course. You must complete the exercises to get your certificate of achievement.

  • 3

    At the end of the course, you will get an email with your results. You will also get a certificate of achievement if you are a

The author

Check out this course in other formats...

Example of certificate of achievement
Example of certificate of achievement