Partager via


Re-bienvenue dans C++ - C++ moderne

Depuis sa création, C++ est devenu l’un des langages de programmation les plus utilisés dans le monde. Les programmes C++ bien écrits sont rapides et efficaces. La langue est plus souple que d’autres langues : elle peut fonctionner au plus haut niveau d’abstraction et descendre au niveau du silicium. C++ fournit des bibliothèques standard hautement optimisées. Il permet d’accéder aux fonctionnalités matérielles de bas niveau, d’optimiser la vitesse et de réduire les besoins en mémoire. C++ peut créer presque n’importe quel type de programme : Jeux, pilotes d’appareils, HPC, cloud, bureau, applications incorporées et mobiles, etc. Même les bibliothèques et les compilateurs pour d’autres langages de programmation sont écrits en C++.

L'une des spécifications d'origine du C++ était la compatibilité descendante avec le langage C. Par conséquent, C++ a toujours autorisé la programmation de style C, avec des pointeurs bruts, des tableaux, des chaînes de caractères terminées par null et d’autres fonctionnalités. Ils peuvent permettre de bonnes performances, mais peuvent également générer des bogues et de la complexité. L’évolution de C++ a mis l’accent sur les caractéristiques qui réduisent considérablement la nécessité d’utiliser des idiomes de style C. Les anciennes installations de programmation C sont toujours là quand vous en avez besoin. Toutefois, dans le code C++ moderne, vous devez en avoir besoin moins et moins. Le code C++ moderne est plus simple, plus sûr, plus élégant et toujours aussi rapide que jamais.

Les sections suivantes fournissent une vue d’ensemble des principales fonctionnalités de C++moderne. Sauf indication contraire, les fonctionnalités répertoriées ici sont disponibles dans C++11 et versions ultérieures. Dans le compilateur Microsoft C++, vous pouvez définir l’option du compilateur pour spécifier la /std version de la norme à utiliser pour votre projet.

Ressources et pointeurs intelligents

L’une des principales classes de bogues dans la programmation de style C est la fuite de mémoire. Les fuites sont souvent causées par un échec d’appel delete à la mémoire qui a été allouée avec new. C++ moderne souligne le principe de l’acquisition de ressources est l’initialisation (RAII). L’idée est simple. Les ressources (mémoire du tas, handles de fichiers, sockets, et ainsi de suite) doivent appartenir à un objet. Cet objet crée ou reçoit la ressource nouvellement allouée dans son constructeur et la supprime dans son destructeur. Le principe de RAII garantit que toutes les ressources sont correctement retournées au système d’exploitation lorsque l’objet propriétaire est hors de portée.

Pour faciliter l’adoption des principes RAII, la bibliothèque standard C++ fournit trois types de pointeurs intelligents : std::unique_ptr, std::shared_ptret std::weak_ptr. Un pointeur intelligent gère l’allocation et la suppression de la mémoire qu’il possède. L’exemple suivant montre une classe avec un membre de tableau qui est alloué sur le tas dans l’appel à make_unique(). Les appels à new et delete sont encapsulés par la unique_ptr classe. Lorsqu’un widget objet sort de la portée, le destructeur unique_ptr sera appelé et libérera la mémoire allouée pour le tableau.

#include <memory>
class widget
{
private:
    std::unique_ptr<int[]> data;
public:
    widget(const int size) { data = std::make_unique<int[]>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // lifetime automatically tied to enclosing scope
                        // constructs w, including the w.data gadget member
    // ...
    w.do_something();
    // ...
} // automatic destruction and deallocation for w and w.data

Dans la mesure du possible, utilisez un pointeur intelligent pour gérer la mémoire du tas. Si vous devez utiliser explicitement les opérateurs et delete les new opérateurs, suivez le principe de RAII. Pour plus d’informations, consultez La durée de vie de l’objet et la gestion des ressources (RAII).

std::string et std::string_view

Les chaînes de style C sont une autre source majeure de bogues. En utilisant std::string et std::wstring, vous pouvez éliminer pratiquement toutes les erreurs associées aux chaînes de style C. Vous bénéficiez également des fonctions membres pour la recherche, l’ajout, le prépending, et ainsi de suite. Les deux sont hautement optimisés pour la vitesse. Lors de la transmission d’une chaîne à une fonction qui nécessite uniquement un accès en lecture seule, en C++17, vous pouvez utiliser std::string_view pour bénéficier d’un avantage de performances encore plus élevé.

std::vector et d’autres conteneurs de bibliothèque standard

Les conteneurs de bibliothèque standard suivent tous le principe de RAII. Ils fournissent des itérateurs pour la traversée sécurisée d’éléments. Et ils sont hautement optimisés pour les performances et ont été soigneusement testés pour l’exactitude. En utilisant ces conteneurs, vous éliminez le risque de bogues ou d’inefficacités qui peuvent être introduits dans des structures de données personnalisées. Au lieu de tableaux bruts, utilisez vector comme conteneur séquentiel en C++.

vector<string> apples;
apples.push_back("Granny Smith");

Utilisez map (pas unordered_map) comme conteneur associatif par défaut. Utilisez set, multimapet multiset pour dégénérer et pour plusieurs cas.

map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

Lorsque l’optimisation des performances est nécessaire, envisagez d’utiliser :

  • Conteneurs associatifs non ordonnés tels que unordered_map. Ils ont une surcharge par élément plus faible et une recherche en temps constant, mais ils peuvent être plus difficiles à utiliser correctement et efficacement.
  • Trié vector. Pour plus d’informations, consultez Algorithmes.

N’utilisez pas de tableaux de style C. Pour les API plus anciennes qui ont besoin d’un accès direct aux données, utilisez plutôt des méthodes d’accesseur.f(vec.data(), vec.size()); Pour plus d’informations sur les conteneurs, consultez conteneurs de bibliothèque standard C++.

Algorithmes de bibliothèque standard

Avant de supposer que vous devez écrire un algorithme personnalisé pour votre programme, passez d’abord en revue les algorithmes de bibliothèque C++ Standard. La bibliothèque standard contient un assortiment toujours croissant d’algorithmes pour de nombreuses opérations courantes telles que la recherche, le tri, le filtrage et la randomisation. La bibliothèque mathématique est vaste. En C++17 et versions ultérieures, des versions parallèles de nombreux algorithmes sont fournies.

Voici quelques exemples importants :

  • for_each, l’algorithme de traversée par défaut (ainsi que les boucles basées sur for des plages).
  • transform, pour la modification non sur place des éléments de conteneur
  • find_if, algorithme de recherche par défaut.
  • sort, lower_boundet les autres algorithmes de tri et de recherche par défaut.

Pour écrire un comparateur, utilisez strict < et utilisez des lambdas nommés lorsque vous le pouvez.

auto comp = [](const widget& w1, const widget& w2)
     { return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );

auto au lieu de noms de types explicites

C++11 a introduit le auto mot clé à utiliser dans les déclarations de variable, de fonction et de modèle. auto indique au compilateur de déduire le type de l’objet afin que vous n’ayez pas à le taper explicitement. auto est particulièrement utile lorsque le type déduit est un modèle imbriqué :

map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

Boucles basées sur for des plages

L’itération de style C sur les tableaux et les conteneurs est sujette à des erreurs d’indexation et est également fastidieuse à taper. Pour éliminer ces erreurs et rendre votre code plus lisible, utilisez des boucles basées sur for des plages avec des conteneurs de bibliothèque standard et des tableaux bruts. Pour plus d’informations, consultez l’instruction basée sur for la plage.

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {1,2,3};

    // C-style
    for(int i = 0; i < v.size(); ++i)
    {
        std::cout << v[i];
    }

    // Modern C++:
    for(auto& num : v)
    {
        std::cout << num;
    }
}

constexpr expressions au lieu de macros

Les macros en C et C++ sont des jetons qui sont traités par le préprocesseur avant la compilation. Chaque instance d’un jeton de macro est remplacée par sa valeur ou expression définie avant la compilation du fichier. Les macros sont couramment utilisées dans la programmation de style C pour définir des valeurs constantes au moment de la compilation. Toutefois, les macros sont sujettes à des erreurs et difficiles à déboguer. Dans C++moderne, vous devez préférer les constexpr variables pour les constantes au moment de la compilation :

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

Initialisation uniforme

En C++moderne, vous pouvez utiliser l’initialisation d’accolades pour n’importe quel type. Cette forme d’initialisation est particulièrement pratique lors de l’initialisation de tableaux, de vecteurs ou d’autres conteneurs. Dans l’exemple suivant, v2 est initialisé avec trois instances de S. v3 est initialisé avec trois instances qui sont elles-mêmes initialisées à l’aide d’accolades S . Le compilateur déduit le type de chaque élément en fonction du type déclaré de v3.

#include <vector>

struct S
{
    std::string name;
    float num;
    S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
    // C-style initialization
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    // Modern C++:
    std::vector<S> v2 {s1, s2, s3};

    // or...
    std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

Pour plus d’informations, consultez Initialisation d’accolades.

Sémantique de mouvement

C++ moderne fournit une sémantique de déplacement, ce qui permet d’éliminer les copies de mémoire inutiles. Dans les versions antérieures de la langue, les copies étaient inévitables dans certaines situations. Une opération de déplacement transfère la propriété d’une ressource d’un objet à l’autre sans effectuer de copie. Certaines classes possèdent des ressources telles que la mémoire du tas, les handles de fichiers, etc. Lorsque vous implémentez une classe propriétaire de ressource, vous pouvez définir un constructeur de déplacement et un opérateur d’affectation de déplacement pour celui-ci. Le compilateur choisit ces membres spéciaux pendant la résolution de surcharge dans les situations où une copie n’est pas nécessaire. Les types de conteneurs de bibliothèque standard appellent le constructeur de déplacement sur les objets si un conteneur est défini. Pour plus d’informations, consultez Les constructeurs de déplacement et les opérateurs d’affectation de déplacement (C++).

Expressions lambda

Dans la programmation de style C, une fonction peut être passée à une autre fonction à l’aide d’un pointeur de fonction. Les pointeurs de fonction sont peu pratiques à gérer et à comprendre. La fonction à laquelle ils font référence peut être définie ailleurs dans le code source, loin du point auquel elle est appelée. En outre, ils ne sont pas de type sécurisé. C++ moderne fournit des objets de fonction, des classes qui remplacent l’opérateur operator() , ce qui leur permet d’être appelées comme une fonction. Le moyen le plus pratique de créer des objets de fonction est d’utiliser des expressions lambda inline. L’exemple suivant montre comment utiliser une expression lambda pour passer un objet de fonction, que la find_if fonction appelle sur chaque élément du vecteur :

    std::vector<int> v {1,2,3,4,5};
    int x = 2;
    int y = 4;
    auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

L’expression [=](int i) { return i > x && i < y; } lambda peut être lue en tant que « fonction qui prend un seul argument de type int et retourne une valeur booléenne qui indique si l’argument est supérieur x et inférieur à y». Notez que les variables x et y du contexte environnant peuvent être utilisées dans l’expression lambda. Spécifie [=] que ces variables sont capturées par valeur ; en d’autres termes, l’expression lambda possède ses propres copies de ces valeurs.

Exceptions

C++ moderne met l’accent sur les exceptions, et non sur les codes d’erreur, comme meilleure façon de signaler et de gérer les conditions d’erreur. Pour plus d’informations, consultez les meilleures pratiques modernes en C++ pour la gestion des exceptions et des erreurs.

std::atomic

Utilisez le struct de bibliothèque std::atomic standard C++ et les types associés pour les mécanismes de communication entre threads.

std::variant (C++17)

Les unions sont couramment utilisées dans la programmation de style C pour conserver la mémoire en permettant aux membres de différents types d’occuper le même emplacement de mémoire. Toutefois, les unions ne sont pas sécurisées de type et sont sujettes à des erreurs de programmation. C++17 introduit la std::variant classe comme une alternative plus robuste et sûre aux unions. La std::visit fonction peut être utilisée pour accéder aux membres d’un variant type de manière sécurisée.

Voir aussi

Informations de référence sur le langage C++
Expressions lambda
Bibliothèque C++ standard
Conformité du langage Microsoft C/C++