CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Concepts des langages de programmation, mise en œuvre en C/C++ - Compléments







  • Rappels :
    • les références sur valeur gauche ne sont que très peu utiles pour des définitions de variables.
    • Elles sont par contre utiles, si ce n'est indispensables (constructeur de copie), pour typer les paramètres d'une fonction.





  • Les références sur valeur droite peuvent être liées à des données temporaires qui pourront être modifiées (pas de const).





  • Pour le passage d'arguments, l'utilisation d'une référence sur valeur droite signale que l'argument reçu va être détruit juste après.
  • L'intérêt est qu'il va être possible de lui « voler » des ressources qu'il possède avant qu'il ne les libére lors de sa destruction (RAII).
  • Il n'y a donc aucun intérêt avec un int qui n'a aucune ressource.





  • L'idiome du swap pour l'opérateur d'affectation a déjà été présenté.





  • Si on essaye d'écrire ce coportement swap de manière générique, on s'aperçoit qu'il se traduit pas 3 copies.
  • Et les données sources de ces 3 copies sont détruites juste après, soit par affectation dans l'instruction suivante, soit par destruction de la variable locale en sortie de bloc.
  • On comprends dès lors que s'il était possible de « voler » la représentation de ces données sources au lieu de la copier, les performances seront améliorées.





  • std::move converti son argument en référence sur valeur droite, qui pourra donc être passée comme argument via un paramètre de ce type.
  • Si le type concerné possède les bonnes méthodes (constructeur, affectation) utilisant un passage par référence sur valeur droite, elles seront utilisées ; sinon, le compilateur se rabattra sur celles avec passage par référence sur valeur gauche constante.
  • Pour être cohérent avec la sémantqiue des données temporaires, les valeurs gauches converties en (références sur) valeurs droites ne doivent plus être utilisées : les seules opérations possibles sont la destruction et l'affectation (équivalent à la destrcution suivie de la construction par copie).





  • On appelle constructeur par déplacement (move constructor) et affectation par déplacement (move assignment) ces opérations qui reçoivent leur argument sour la forme d'une référence sur valeur droite.
  • À noter que si l'affectation par copie utilise un passage par valeur, une définition d'une affectation par déplacement entraine une erreur d'ambiguïté signalée par le compilateur.





  • Dans le cas de notre pile, on comprend facilement l'intérêt de les définir : la pile reçue en argument va être détruite juste après, donc on peut lui « voler » sa représentation au lieu d'en faire une copie.





  • Ce fichier dans le dépôt Git montre l'utilisation de ces opérations par déplacement lors de l'utilisation de swap.





  • Ce tableau liste tous les opérateurs du langage C++.
  • L'opérateur de comparaison à trois voies (three way comparison operator) <=> a été introduit par la version 2020 du standard du langage C++. C'est un opérateur binaire permettant de déterminer en une seule instruction si les deux opérandes sont égaux, si l'un est plus petit, ou s'ils sont incomparables (voir plus loin).
  • Les opérateurs .* et ->* servent quand on utilise un pointeur sur membre à la place du nom du membre.
  • L'opérateur ,, qui provient du langage C, est l'opérateur binaire de séquencement : il évalue son premier argument, puis son second qui sera le résultat de l'expression. À noter que la virgule est aussi utilisée comme séparateur d'argument ; ainsi, il ne faut pas confondre f(a, b) avec f((a, b)).
  • L'opérateur conditionnel ? :, qui provient lui aussi du langage C, est un if-then-else : dans l'expression a ? b : c, a est évalué, s'il est vrai, le résultat sera l'évaluation de b, sinon l'évaluation de c.
  • :: n'est que partiellement un opérateur car ses opérandes ne sont pas des expressions.





  • L'opérateur appel de fonction () a une arité quelconque.
  • Tous les opérateurs unaires sont préfixés, sauf ++ et -- qui existent en préfixé et en postfixé.
  • La majorité des opérateurs sont associatifs à gauche, l'opérateur d'affectation = et ses versions composées sont associatifs à droite.
  • Seuls les opérateurs logiques && et || (évaluation en court-circuit), l'opérateur de séquencement et l'opérateur ternaire ? : imposent un ordre d'évaluation.





  • Le langage C++ permet la surcharge de la majorité des opérateurs ; au moins l'un des opérandes doit être d'un type utilisateur : classe ou énumération.
  • La définition d'une fonction libre ou membre operator ∆ permet d'utiliser la syntaxe habituelle de l'opérateur .
  • Le programmeur a en général le choix entre une approche fonctionnelle et une approche objet.

Bonne pratique

  • La surcharge permet l'écriture naturelle de certaines expressions, elle doit donc être utilisée pour améliorer la lisibilité d'un code.





  • Les opérateurs ne pouvant être surchargés sont : ., .*, ? : et ::.
  • Il est préférable d'éviter la surcharge de && et || car on perd l'évaluation en court-circuit (la surcharge se traduit par un appel de fonction, les deux opérandes sont évalués).









  • Pour les opérateurs arithmétiques, le programmeur a le choix entre l'approche objet et l'approche fonctionnelle.
  • friend déclare que la fonction libre ainsi qualifiée est amie de la classe ; en tant qu'amie, elle a accès aux membres privés. Une fonction membre ou une classe (donc toutes ses fonctions membres) peuvent aussi être déclarées comme amies.





  • Le constructeur de Nombre est utilisé comme opérateur de conversion implicite d'un int vers un Nombre (même domaine). Mais le compilateur refuse d'effectuer cette conversion pour créer un objet temporaire à qui un message sera envoyé.

Bonne pratique

  • L'approche fonctionnelle est préférable pour la surcharge des opérateurs arithmétiques binaires.





  • Les opérateurs ++ et -- existent en préfixé et en postfixé ; pour permettent la surcharge des deux versions, la fonction surchargeant la version postfixée a un argument supplémentaire de type int dont la valeur est sans signification.





  • Les opérateurs de comparaison sont liés entre eux, il n'y a aucune raison vallable de ne pas respecter cette sémantique s'ils sont surchargés.
  • Avant C++ 2020, la définition de == et < permet d'obtenir une définition automatique des autres via les définitions de l'espace std::rel_ops.





  • L'opérateur de comparaison à trois voies <=> introduit par C++ 2020 permet la définition d'un ordre en ne surchargeant que cet opérateur.
  • Le type de résultat permet 3 sortes d'ordres :
    • un ordre total fort (std::strong_ordering) qui implique la substituabilité (si a est équivalent - ou égal : synonyme - à b, alors f(a) est équivalent à f(b)),
    • un ordre total faible (std::weak_ordering), sans substituabilité,
    • un ordre partiel (std::partial_ordering) où toutes les valeurs ne sont pas comparables.





  • La surcharge de l'affectation (par copie ou par déplacement) a déjà été vue.
  • Les affectations composées doivent aussi obligatoirement utiliser l'approche objet, elles permettent ensuite de définir facilement les opérateurs arithmétique binaires sous forme fonctionnelle sans avoir besoin de les déclarer amis.





  • L'opérateur d'indexation est bien sûr utilisé par les conteneurs de la catégorie des tableaux dans la STL, mais aussi par les dictionnaires.





  • L'opérateur -> est un opérateur unaire ; en effet, dans l'expression a->b, seul a est évalué, b est le nom du membre accédé.
  • Sa surcharge permet d'utiliser des objets qui se comportent comme des pointeurs ; on parle de pointeurs intelligents (smart pointers).
  • Si a est un objet, alors a->b est converti en a.operator->()->b, donc le résultat de -> doit être soit un vrai pointeur, soit un objet qui a aussi ce comportement ->.





  • Cet exemple basique montre à quoi peut servir un pointeur intelligent pour éviter des fuites de mémoire.





  • Le standard C++ a défini deux sorte de connaissance d'un objet.
  • std::shared_ptr offre la connaissance partagée : un objet peut être connu par plusieurs, il continue d'exister tant qu'il existe au moins un autre objet qui le connait, il est détruit quand il n'est plus connu de personne.
  • Son implémentation se fait classiquement avec un compteur de références.





  • Les compteurs de références sont une des méthodes permettant une gestion automatique de la mémoire. L'inconvénient de cette approche est que les cycles ne sont pas détectés (a connait b qui connait a, mais personne d'autre ne les connait) et provoquent donc encore des fuites mémoires.
  • Pour casser de tel cycles, on introduit un std::weak_ptr , que l'on peut convertir en shared_ptr (qui sera valide ou pas selon que l'objet existe encore ou pas) quand on en a besoin.





  • std::unique_ptr offre l'appartenance stricte : l'objet connu n'est connu que d'un seul autre, et est détruit avec lui.
  • La propriété de cet objet peut être transférée à un autre : c'est pourquoi le constructeur et l'affectation par copie sont interdits, mais les versions par déplacement sont fournies.





  • Ce fichier dans le dépôt Git montre l'utilisation de ces pointeurs intelligents.





  • Toute bonne pratique a ses exceptions. Dans le cadre de la bibliothèque standard d'entrées/sorties C++, les opérateurs << et >> sont surchargés, mais pas du tout dans le même domaine (arithmétique) ni avec la même sémantique.
  • Cette exception est justifiée par le fait que ces opérateurs sont très peu utilisés et qu'il permettent une écriture simple pour la lecture et l'affichage de données.
  • Nous avons vu dans le premier chapitre comment lire et afficher des entiers et des chaînes de caractères en utilisant cette bibliothèque. Nous souhaitons faire de même avec une classe que nous concevons, Nombre ici ; côté utilisation, le seul changement est celui du type de la variable (avec aussi l'inclusion du fichier d'entête). Nous supposons que la multiplication d'instances de Nombre a été correctement définie.





  • Pour que cela marche, nous avons besoin de surcharger << et >> avec les bons types.
  • L'opérande gauche de >>, std::cin est de type std::istream ; c'est un type appartenant à la bibliothèque standard, nous ne pouvons pas le modifier en y ajoutant une méthode, donc nous retenons l'approche fonctionnelle. Les entrées/sorties fonctionnent par effet de bord en modifiant l'état de l'objet qui représente l'entrée ou la sortie, pas une copie de celui-ci ; donc il doit être reçu par référence sur valeur gauche non constante.
  • L'opérande droit de >> est une instance de Nombre qui va être modifiée par l'opération de lecture : là aussi, passage par référence sur valeur gauche non constante.
  • Pour pouvoir chaîner les opérations de lecture ou d'affichage, notre surcharge de >> doit retourner l'objet qui représente l'entrée, pas une copie : retour d'une référence sur valeur gauche non constante.
  • Avec le même raisonnement pour l'affichage (std::cout est de type std::ostream), mais en tenant compte que, cette fois, l'instance de Nombre ne doit pas être modifiée, nous arrivons à la signature de la fonction en charge de l'affichage.





  • new et delete (et aussi new[] et delete[]) sont considérés comme des opérateurs, et peuvent être à ce titre surchargés. Il y a 2 possibilités :
    • S'ils sont surchargés comme fonctions membres d'une classe, celles-ci seront utilisées pour l'allocation et la libération des instances directes ou indirectes de la classe.
    • S'ils sont surchargés comme fonctions libres, il est possible d'ajouter des arguments en plus de celui qui donne la taille de la zone mémoire à allouer (pour new) ou l'adresse de la zone à libérer (pour delete) ; en particulier, la bibliothèque contient une version, dite placement new, qui reçoit un pointeur générique comme argument supplémentaire et qui ce contente de le retourner : cette astuce permet d'imposer la zone mémoire où un objet sera mémorisé en conservant les bénéfices de la création de l'objet, en particulier l'appel d'un constructeur.





  • Ce fichier dans le dépôt Git montre un exemple de surcharge de ces opérateurs comme fonctions membres.





  • Nous avons vu que les algorithmes de la STL travaillent avec une paire d'itérateurs pour leur permettre de travailler sur une partie d'un conteneur.
  • C++ 2020 a introduit le concept de range qui généralise ce qui est itérable :
    • une paire d'itérateurs,
    • un itérateur et un nombre d'éléments,
    • un itérateur et un prédicat,
    • un seul itérateur donnant accès à des séquences potentiellement infinies (évaluation paresseuse),
    • une view qui donne accès en lecture seule à des éléments sélectionnés d'une séquence sous-jacente.





  • Cet exemple, qui n'utilise pas cette nouvelle bibliothèque, affiche les carrés des éléments impairs d'une séquence existante.
  • Il est nécessaire de passer par des séquences intermédiaires avec le coût de la recopie.





  • Le même exemple avec la nouvelle bibliothèque.
  • r1 et r2 sont des vues sur la séquence data sous-jacente, les éléments de celle-ci ne sont pas copiés.
  • La surcharge de l'opérateur | permet une syntaxe naturelle pour les habitués des shells Unix.





  • Ce fichier dans le dépôt Git montre les 3 versions de cet exemple.





  • Ce fichier dans le dépôt Git montre un générateur (évaluation paresseuse d'une suite).









  • Ce fichier dans le dépôt Git montre des exemples de manipulations de sacs.









© 2023 CentraleSupélec