Le C++ a fait beaucoup de chemin depuis les années 1980, et beaucoup d’améliorations significatives ont été apportées au langage avec le standard C++11, sorti en 2011.

En tant que développeur C++, vous allez très certainement apprécier les 11 nouvelles fonctionalités présentées ci-dessous (mais il y en a bien d’autres !).

1) Le type “auto”

Fatigué des déclarations de variables à rallonge telles que std::map<std::string, std::pair<int, int> >::const_iterator iter; ?

Le C++11 a introduit le type auto, qui devine automatiquement le type d’une variable en fonction du contexte. Appréciez:

std::map<std::string, std::pair<int, int> > myMap;
...
auto iter = myMap.find(42);

2) Les boucles “for” sur intervalle

Tous les languages modernes supportent les boucles de types “foreach”, qui permettent d’itérer facilement sur une collection quelconque. C’est maintenant également le cas pour le C++:

std::list<std::string> myList;
...
for (auto item: myList)
{
    std::cout << item << std::endl;
}

3) Les listes d’initialisation étendues

Les conteneurs STL standards peuvent maintenant être initialisés facilement avec des accolades (ce qui était auparavant possible uniquement avec des objets de type POD):

std::vector<int> myVector = {1, 3, 5, 7};

ou même:

std::vector<int> myVector {1, 3, 5, 7};

4) Initialisation uniforme

En C++11, les accolades peuvent êtres utilisées de la même façon pour initialiser n’importe quoi, que ce soit un conteneur STL, un objet, un tableau, un tableau d’objets, etc.

struct Foo 
{
    Foo(int n, std::string s): m_n(n), m_s(s) {}
    int m_n;
    std::string m_s;
};

Foo myObject {12, "foo"};
Foo myArray[] {{12, "foo"}, {42, "bar"}};
std::vector<Foo> myVector {{12, "foo"}, {42, "bar"}};

Quand le type de l’objet peut être deviné par le compilateur, par exemple pour la valeur de retour d’une fonction, il n’est même pas nécessaire de spécifier le type lors de l’initialisation !

std::pair<int, std::string> foo()
{
    return {12, "foo"};
}

auto myPair = foo();

5) Expressions lambda et closures

Une expression lambda est simplement une fonction anonyme, qui peut-être définie à n’importe quel endroit du code s’attendant à recevoir un pointeur de fonction.

Les expressions lambda sont très utiles pour la programmation fonctionnelle, pour éviter de définir des fonctions très petites qui ne sont appelées qu’une seule fois.

Par exemple, pour incrémenter tous les éléments d’un vecteur, on peut écrire:

std::vector<int> myVec {1, 2, 3, 4};
std::for_each(myVec.begin(), myVec.end(), [](int &n) {n++;});

Ici, [] est l’équivalent de lambda en Python ou function en Javascript.

Pourquoi cette syntaxe étrange [] ? Parce qu’il est en fait possible de mettre quelque chose à l’intérieur des crochets, pour créer une closure.

Une closure est une expression lambda qui capture une ou plusieurs variables locales définies en dehors de sa portée. En Python et en Javascript, toutes les variables locales définies dans la même portée qu’une expression lambda sont capturées implicitement, mais comme les développeurs C++ aiment tout contrôler (sinon ils ne développeraient pas en C++!), la liste des variables à capturer dans une closure C++11 doit être specifiée explicitement à l’intérieur des crochets.

Par exemple, si on veut incrémenter tous les éléments de notre vecteur d’un certain nombre delta, on peut écrire:

std::vector<int> myVec {1, 2, 3, 4};
int delta = 4;
std::for_each(myVec.begin(), myVec.end(), [delta](int &n) {n += delta;});

Remarquez que si la variable delta n’est pas spécifiée dans la liste de capture, le compilateur produit une erreur.

Par défaut, les variables sont capturées par valeur, mais elles peuvent aussi être capturées par référence. Par exemple, on peut calculer la somme des éléments d’un vecteur en utilisant une closure comme ceci:

std::vector<int> myVec {1, 2, 3, 4};
int sum = 0;
std::for_each(myVec.begin(), myVec.end(), [&sum](int &n) {sum += n;});

6) Chevrons intelligents

Quand on manipule des templates, on peut maintenant écrire std::map<std::string, std::pair<int, int>> au lieu de std::map<std::string, std::pair<int, int> >.

Cela peut sembler être une amélioration mineure, mais qui n’a jamais été irrité par ces stupides espaces ?

7) Expressions régulières

Les expressions régulières sont maintenant fournies par la bibliothèque standard <regex>.

Voici un exemple montrant comment repérer un mot et un nombre dans une chaîne donnée:

#include <regex>

std::regex myRegex(R"((\w+)=(\d+))");
std::string myStr("n=42");
std::smatch match;
std::regex_match(myStr, match, myRegex);
for (std::string s: match)
{
    std::cout << s << std::endl;
}

Ceci produit la sortie suivante:

n=42
n
42

On notera au passage la nouvelle syntaxe de chaîne littérale R"(...)" qui permet d’éviter les doubles échappements \\ (similaire à r'...' en Python par exemple).

8) Pointeurs intelligents

La gestion correcte de la mémoire est un des aspects les plus complexes du C++, mais heureusement les pointeurs intelligents (smart pointers) sont là pour nous faciliter la vie.

Le principes des pointeurs intelligents est de s’assurer que les objets sont détruits automatiquement quand ils ne sont plus utilisés. Le C++11 a introduit deux types de pointeurs intelligents: unique_ptr et shared_ptr (et son frère jumeau le weak_ptr).

unique_ptr est un remplacement d’auto_ptr, qui est maintenant déprécié car il peut être dangereux à utiliser. Un unique_ptr détient la propriété d’un pointeur, et comme le nom l’indique, un pointeur peut appartenir à un seul unique_ptr à un moment donné. Cependant, cette propriété peut être transférée explicitement à un autre unique_ptr, en utilisant la fonction move:

#include <memory>

class Foo {...};

void bar() 
{
    std::unique_ptr<Foo> myPtr(new Foo);
    myPtr->someMethod();
    {
        // Transfère la propriété du pointeur
        std::unique_ptr<Foo> myOtherPtr = std::move(myPtr);
        myOtherPtr->someMethod();
        // L'objet Foo est détruit automatiquement ici
    }
    // Ne pas utiliser myPtr à partir d'ici !
}

shared_ptr, emprunté a la bibliothèque Boost, est un pointeur à comptage de références: plusieurs instances de shared_ptr peuvent partager la propriété du même objet, et cet objet est détruit seulement quand la dernière référence vers celui-ci est détruite.

#include <memory>

class Foo {...};

void bar() 
{
    std::shared_ptr<Foo> myPtr(new Foo);
    myPtr->someMethod();
    {
        // Partage la propriété du pointeur
        std::shared_ptr<Foo> myOtherPtr = myPtr;
        myOtherPtr->someMethod();
    }
    myPtr->someMethod();
    // L'objet Foo est détruit automatiquement ici
}

9) Support des threads

La STL en C++ contient une bibliothèque <thread>, avec tous les outils de base nécessaires au multi-threading: threads, mutexes, lock guards, et même futures.

Voici un petit exemple montrant commencer lancer un thread qui dort pendant n secondes:

#include <iostream>
#include <chrono>
#include <thread>

void foo(int n) 
{
    std::chrono::seconds sec(n);
    std::this_thread::sleep_for(sec);
    std::cout << "deux" << std::endl;
}
 
int main()
{
    std::thread myThread(foo, 5);
    std::cout << "un" << std::endl;
    myThread.join();
}

The programme affiche “un”, puis “deux” 5 secondes plus tard.

10) Tables de hachage

Dans la STL, les maps et les sets sont implémentés avec des arbres binaires, qui sont des conteneurs ordonnés, ce qui permet de rechercher efficacement une clé dans un intervalle de valeurs.

Quand l’order n’est pas nécessaire, il est généralement plus efficace d’utiliser des tables de hachage à la place des arbres binaires (en moyenne, le complexité d’une recherche dans une table de hachage est en O(1), alors qu’une recherche dans un arbre binaire est en O(log n))

En C++11, les tables de hachages sont implémentées dans des nouveaux conteneurs: unordered_map, unordered_set, unordered_multimap and unordered_multiset.

11) La sémantique de déplacement

Le concept de déplacement (move) est une amélioration importante de C++11 pour simplifier l’écriture de programmes efficaces.

Avant le C++11, il était absolument déconseillé de passer comme valeur de retour d’une fonction des “gros” objets, par exemple un vector contenant beaucoup d’éléments.

En effet, dans le code suivant:

std::vector<std::string> foo()
{
    ...
    return aBigVector;
}

std::vector<std::string> myVector = foo();

la fonction foo() renvoie un objet temporaire, qui est a priori copié dans la variable myVector, ce qui est une opération coûteuse. En réalité dans ce cas le compilateur peut effectuer une optimisation appelée RVO (Return Value Optimization), qui permet d’éviter une copie, mais cette optimisation est laissée au choix du compilateur et n’est pas garantie.

En C++11, un nouveau type de constructeur est apparu: le constructeur par déplacement, dont la signature est de la forme vector(vector&& other).

La syntaxe vector&& désigne une référence rvalue, par opposition aux références habituelles (de type vector&) qui sont des références lvalue. Les références rvalue sont utilisées pour référencer des objets temporaires, comme la valeur de retour dans la fonction foo() dans l’exemple précédent.

Le constructeur par déplacement prend donc en paramètre un objet temporaire, et le déplace dans un nouvel objet. L’opération de déplacement transfère les données de l’ancien objet vers le nouveau, sans effectuer de copie.

Par exemple, un vector contient en interne un pointeur vers un tableau, qui lui contient les vraies données. Quand on appelle le constructeur par copie de vector, les éléments du tableau sont copiés un à un (en appelant le constructeur de copie de chaque élément), ce qui peut être très pénalisant en termes de performance.

Avec le constructeur par déplacement, seul le pointeur vers le tableau interne est copié dans le nouvel objet, mais pas le tableau lui-même; l’opération est donc très rapide. Après un déplacement, l’ancien objet n’est pas détruit mais est en quelque sorte “vidé” de ses données, et devient donc inutilisable.

En C++11, le code std::vector<std::string> myVector = foo(); appelle automatiquement le constructeur par déplacement s’il existe (ce qui est le cas pour vector et tous les conteneurs STL), au lieu du constructeur par copie.