Le kit de survie du C++ moderne

  • 1 Réponses
  • 75 Vues
*

Hors ligne Alairion

Le kit de survie du C++ moderne
« le: 12 mai 2018, 23:57:39 »
Bonjour à tous,
À force de me promener sur divers forums, j'ai remarqué que bon nombre de personnes utilisaient C++ sans forcément connaître les nouveautés  essentielles des dernières normes du langage, ainsi que les bonnes pratiques pour programmer en C++ moderne.
Ce post traite aussi un peu de fonctionnalités parfois plus vieilles que C++11, mais je passerai vite dessus.
Je sais que certains utilisent, au moins un peu, ce langage. Je vais donc mettre en avant quelques fonctionnalités disponibles en C++ moderne (C++11, 14 et 17), certaines informations vont être très simplifiées car mon but et juste de donner un aperçu rapide de celles-ci.
Ce post peut être intéressant même pour ceux n'utilisant pas ce langage, il permet de voir que C++ n'est pas forcément le langage que beaucoup de personnes pensent qu'il est...
Les liens donnés ici vont vers cppreference, une très bonne doc pour la bibliothèque standard du C++, ou vers l'entrepôt de tout le savoir et de toutes les vérités, j'ai nommé: Wikipedia !

Les indispensables de la bibliothèque standard

On va commencer par là où tout le monde devrait commencer: la bibliothèque standard ! Elle dispose de beaucoup de fonctionnalités qui sont souvent méconnues, on va donc faire un rapide tour de ce qu'elles apportent.
En premier lieu, nous allons voir les indispensable. Ce sont des en-tête que vous retrouverez dans tout programme C++ qui se respecte:
  • <string> et <string_view>: Gestion des chaînes de caractères
  • <vector>: Gestion de tableau à taille dynamique
  • <array>: Gestion de tableau à taille connu à la compilation
  • <memory>: Utilitaires de gestion de l'allocation dynamique
  • <utility>: Divers objets et fonctions simplifiant la vie
  • <algorithm>: Une bonne poignée (87) d'algorithmes courants
<string> et <string_view>
Je pense que le premier n'a pas besoin d'être présenté, mais dans le doute je vais préciser qu'il s'agit d'un conteneur à taille dynamique qui est spécialisé dans le stockage de caractères et dispose de nombreuses fonctions pour remplir son rôle, comme l'addition de chaînes par exemple. Le second quand à lui est un petit nouveau du C++17, il est là pour représenter une instance de string qui ne possède pas ce vers quoi il pointe. Il permet d'avoir une référence vers un C-string (const char*), il ne stocke que le pointeur et la taille de la chaîne pointée, ainsi il est très léger à copier et est idéale pour le passage de chaînes constantes à une fonction, pour remplacer ce bon vieux "const std::string&". Voici un exemple d'utilisation de std::string_view:

#include <string_view>
#include <string>
#include <iostream>
   
void print(std::string_view strv)
{
    std::cout << std::data(strv) << std::endl;
}
   
int main()
{
    print(u8"Hello World!"); //Construct the string view with the null-terminated character literal (C-string) "Hello World!"
   
    std::string str{u8"Hello, it's me..."};   
    std::string_view strv{std::data(str), std::size(str)}; //Take a reference over str, std::basic_string::data returns a null-terminated string sincec C++11
    print(strv);
}
Petit bonus: il est important de connaître l'existence de la SSO (Small-String-Optimization) qui alloue les chaînes de courtes tailles sur la pile au lieu du tas, ainsi il y a de forte chance que mon "Hello World!" soit en fait alloué sur la pile.

<vector>
Maintenant que nous avons vu un conteneur à taille dynamique spécialisé dans le stockage de caractères, passons à quelque chose de plus générique.
C'est un icône du C++ qui marque le début d'une nouvelle ère. En réalité, je vais même pas le présenté, il est trop connu pour qu'on lui fasse un tel affront. De plus la documentation s'en occupe très bien. Quoi qu'il en soit utilisez-le, il est là pour ça. Pour les curieux qui ne connaissent pas C++ et qui désirent un exemple d'utilisation il y en aura un dans la partie suivante je l'utiliserai comme exemple pour le "RAII".

<array>
Nous allons, désormais, rencontré le petit cousin de vector, il s'appelle std::array !
Il partage beaucoup de point commun avec son cousin, il a juste le dynamisme en moins. Comme il est né en 2011, il aura quand à lui droit à un plus ample présentation. Un std::array contient donc un nombre N d'éléments de type T. Le tout est toujours alloué sur la pile (si il y en a une évidemment). Il doit toujours être utilisé à la place des arrays du C, car il évite beaucoup de bêtises (VLA, conversion implicite vers pointeur, ...) et qu'il s'associe à merveille avec les templates. Pour l'instant prenons l'exemple suivant pour voir à quoi ressemble ce petit tableau:

#include <array>
   
int main()
{
    std::array<char, 16> str; //An empty array of 16 chars.
    std::array<double, 5> arr{3.14, 1.42, 42.42, 12.5, 99.9}; //An array of 5 double
    auto arr2 = arr; //Copy
    bool equal{arr == arr2}; //Compare
}

<memory>
C'est l'en-tête la plus importante du C++11. Elle définie, entre-autres, les pointeurs intelligents qui sont aussi un indispensable du C++. Je vais vraiment me concentrer sur eux. Mais qui sont ces pointeurs ? Et pourquoi intelligents ? Leur petit nom sont std::unique_ptr, std::shared_ptr et std::weak_ptr.
Commençons par le commencement, std::unique_ptr. C'est un pointeur qui est le seul propriétaire de ce qu'il pointe et, lorsqu'il est détruit, détruit ce vers quoi il pointe à l'aide d'un "deleter". Le deleter est un objet "callable" (fonction, foncteur, lambda, ...), il est appelé dans le destructeur de std::unique_ptr, qui donne à votre fonction le pointer nu (T*) vers lequel il pointe. Le rôle de cette fonction est de libérer la ressource pointée. Le deleter par défaut utilise simplement "delete" sur le pointeur. Cette mécanique permet d'interfacer très simplement des bibliothèque C, par exemple si vous utilisez SDL2, vous pouvez écrire ceci:

#include <memory>
#include <functional>
#include <SDL2.h>
   
int main()
{  //SDL must be initialized, but i do not because it is an example of std::unique_ptr not SDL2...
    std::unique_ptr<SDL_Window, std::function<void(SDL_Window*)>> ptr{SDL_CreateWindow(...), SDL_DestroyWindow}; //Construct ptr with SDL_CreateWindow. The deleter holds the function SDL_DestroyWindow.
}
std::function est un utilitaire capable de contenir une référence vers un fonction, il peut contenir une fonction libre ou membre, un pointeur sur fonction ou encore une lamdba.
Cette exemple montre un comment interfacer une bibliothèque C, cependant méfiez-vous de la cstdlib. Certaines bibliothèques, telle que STB utilise malloc et free qui sont incompatible avec new et delete, vous devez donc faire ça:

#include <memory>
#include <functional>
#define STB_IMAGE_IMPLEMENTATION //Needed by stb image
#include <stb_image.h>

int main()
{
    std::unique_ptr<unsigned char[], std::function<void(void*)>> ptr{stbi_load(...), std::free};
}
Dans les deux exemple j'utilise explicitement le constructeur std::unique_ptr, mais il est préférable d'utiliser std::make_unique<T> (C++14) pour construire un std::unique_ptr, en particulier si vous le passez en paramètre d'une fonction directement.
De plus je viens très subtilement de vous montrer que les pointeurs intelligents supportent les tableaux, ainsi, si le comportement de std::vector ne vous convient pas, bien que ce soit rare, vous pouvez vous rabattre que les pointeurs intelligents et leur comportement plus "primitif".
Passons à std::shared_ptr et std::weak_ptr, qui vont de pair. std::shared_ptr est le cousin de unique_ptr, tout ce qui s'applique à std::unique_ptr s'applique aussi à std::shared_ptr. Sauf bien entendu, ce qui fait leur différence, la propriété de la ressource pointée. Là ou std::unique_ptr fait le radin et garde tout pour lui, std::shared_ptr lui peut avoir la responsabilité de la même ressources, qu'a un de ses frères. Lorsque plusieurs std::shared_ptr pointent vers la même ressource, c'est le dernier détruit qui libère la ressource à l'aide du deleter. Petit exemple, c'est cadeau:

#include <iostream>
#include <memory>

std::shared_ptr<std::int32_t> foo()
{
    auto ptr = std::make_shared<std::int32_t>(42); //Here, we create a std::shared_ptr with std::make_shared.
    return ptr; //Here the pointer is copied to a new instance, so there is 2 instance of std::shared_ptr which hold the same ressource. When the function is leaved, the first one is destroyed but the second (temporary) is still alive so the ressource is kept.
}

void bar()
{
    std::shared_ptr<std::int32_t> ptr2 = foo(); //ptr2 is the third instance that holds our integer, it is constructed for the temporary std::shared_ptr constructed on the foo's return.
    std::cout << *ptr << std::endl; //Print "42".
}// Here the ptr2 is destroyed, and it is the last one alive, so the ressource is also detroyed.

int main()
{
    bar();
}
Il ne reste plus que std::weak_ptr, c'est un std::shared_ptr, qui n'a pas la propriété de la ressource. Il doit être convertie vers un std::shared_ptr pour pouvoir être utilisé. Comme il ne possède pas la ressource, il peut pointer vers rien, mais comme il fait partie de la hiérarchie de std::shared_ptr il peut être tenu au courant si la ressource est toujours présente. Voici un exemple qui illustre le fonctionnement de std::weak_ptr, cependant il utilise une variable globale, ce qui est une mauvaise pratique dans la réalité:

Et voilà notre petit tour de l'en-tête <memory>.

<utility>
Alors, je ne vais pas détailler son contenu, déjà que ce que j'ai dis au dessus forme un pavé concéquent. Notez juste qu'elle contient quelques fonctions et quelques objets très pratiques, telle que std::swap qui échange deux valeurs, std::exchange qui change une valeur et renvoie l'ancienne valeur ou encore std::move qui est indispensable pour la sémantique de mouvement.

<algorithm>
Tout comme l'en-tête utility, je ne peux pas en faire le tour, il y a trop de contenu. Dans cette en-tête vous trouverez algorithmes de tri, (std::sort),  et de recherche et diverses operations sur des conteneurs (copie, transformation, réoganisation, recherche, ...). Lorsque vous avez besoin d'une opération courante, vérifiez qu'elle n'est pas déjà présente sous forme d'un algorithme.

Mentions honorables de la bibliothèque standard

Voici quelques mentions honorable que je ne détaillerai pas, mais la documentation de cppreference vous donnera bon nombre d'exemples, ces fonctionnalités répondent à un très grand nombre de besoins:
  • <map> et <unordered_map>: Tableau associatif clé-valeur.
  • <list> et <forward_list>: Liste doublement ou simplement liée.
  • <iostream>: Les flux standards.
  • <random>: Générateurs de nombres pseudo-aléatoires.
  • <regex>: Utilitaire de gestion des expressions régulières.
  • <filesystem>: Diverses classes et fonctions pour gérer fichiers et dossiers.
  • <thread>, <mutex>, <shared_mutex>, <future>, <condition_variable> et <atomic>: Plein de fonctionnalités pour gérer les threads.
  • <chrono> : De quoi manipuler le temps.
  • <any>, <optional> et <variant> : Trois petits nouveaux du C++17 qui se sont révélés plus pratiques qu'ils en ont l'air. Le premier peut représenter n'importe quoi, le deuxième une valeur qui n'est pas toujours présente et le troisième est un remplaçant sûr des unions.
  • <tuple> : Une version plus globale de std::pair qui permet contenir des valeurs de types différents sous un seul conteneur.
  • <type_traits> : Permet d'obtenir des informations sur les types à la compilation. Très utile avec les templates.
  • <exception> : Definie différents types d'exceptions, et donne des alternatives pour la gestion des excetions.
Le RAII

Alors le RAII. C'est quelque chose de très important en C++. Mais c'est quoi le RAII ? Telle est la question. RAII est un acronyme de "Resource acquisition is initialization", il consiste, en gros, au principe suivant: le constructeur donne une instance d'un type T valide, et son destructeur libère toutes les ressources liées à cette instance. Notez aussi que dans beaucoup de cas, si le constructeur ne peux pas créer une instance valide, il lancera une exception. Toutes bonnes classes se doit de suivre le RAII et la bibliothèque standard est un parfait exemple d'utilisation du RAII. Les pointeurs intelligents sont la définition même du RAII au même titre que les conteneurs. Personnelement je résume ça comme ça, mais il ne faut pas tout le temps régler le problème ainsi: "Si le constructeut ne parvient pas à vous chier une instance valide, il vous vomit une exception à la gueule.", charmant non ? Certaines classes n'utilisent pas les exception en cas d'erreur de construction, c'est le cas de std::basic_fstream, qui est un flux sur un fichier, il y a tellement de facteurs pouvant empêcher l'ouverture d'un fichier qu'un exception serait de trop, la bibliothèque standard met juste le flux dans un état invalide mais défini. Voici un exemple avec des classes qui utilisent le RAII:

#include <vector>
#include <fstream>

int main()
{
    std::ifstream ifs{"readable_file.txt", std::ios_base::ate}; //We try to open a file, and move to the end to read is size
    if(!fs) //If the file can not be opened westop the program
        return 1;
   
    std::vector<char> file_data{static_cast<std::size_t>(ifs.tellg())}; //We create a std::vector which fit the size of the file
    ifs.seekg(0, std::ios_base::beg); //We move to the beggining of the file
   
    ifs.read(std::data(file_data), std::size(file_data)); //We read the entire file, and store it into our std::vector
}//Here the std::ifstream is destroyed, so the file handle returns to the system, the std::vector is also destroyed so the memory returns to the system too.
Voilà il n'y a pas grand chose à ajouter. C'est un concept assez simple, mais qui est un fondement du C++, il faut donc le connaître et savoir l'utiliser

Conclusion

Voilà, c'est la fin de notre tour express du code de base du C++ moderne. J'espèce que cet article vous aura plu, malgrès le fait qu'il est plutôt long et pas forcément très amusant ! N'oubliez pas que je n'ai que frollé la surface de cet océan qu'est C++. J'ai juste montré ici les principes fondamentaux pour faire du C++ moderne et il manque beaucoup de choses. D'ailleurs cet article devait être beaucoup plus long, mais je me suis mangé un mur nommé limite de caractères :ahde:.
Pour ceux voulant découvrir encore plus de choses voici quelques pointeurs vers des choses intéressantes:
Et je vais m'arrêter là car je m'approche dangereusement des 20000 caractères.  En tout cas si vous avez des questions n'hésitez surtout pas à les poser ! Bonne journée à vous et soyez intelligents !
« Modifié: 13 mai 2018, 14:06:55 par Alairion »
Oui
 
Utilisateurs ayant remercié ce post : Aerun, Deakcor, Vulvoch, yyyyj, Leikt

*

Hors ligne SirMalo

Le kit de survie du C++ moderne
« Réponse #1 le: 13 mai 2018, 10:13:13 »
Je ne touche pas (encore) à la programmation, mais je ne peux que te féliciter pour la clarté de ta mise en page et de ton écriture ! Je pense et j'espère que ton tuto en aidera plus d'un.
 
Utilisateurs ayant remercié ce post : Alairion