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.
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é.
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 !
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 :
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 :
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 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 :
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. :-° )
É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).
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 :
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 :
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 :
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 :
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;
}
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 :
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 :
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 :
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.
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.