- 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 confondref(a, b)
avecf((a, b))
. - L'opérateur conditionnel
? :
, qui provient lui aussi du langage C, est unif-then-else
: dans l'expressiona ? b : c
,a
est évalué, s'il est vrai, le résultat sera l'évaluation deb
, sinon l'évaluation dec
. ::
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'unint
vers unNombre
(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 typeint
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'espacestd::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é (sia
est équivalent - ou égal : synonyme - àb
, alorsf(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.
- un ordre total fort (
- 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'expressiona->b
, seula
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, alorsa->b
est converti ena.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
connaitb
qui connaita
, 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 enshared_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 deNombre
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 typestd::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 deNombre
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 typestd::ostream
), mais en tenant compte que, cette fois, l'instance deNombre
ne doit pas être modifiée, nous arrivons à la signature de la fonction en charge de l'affichage.
new
etdelete
(et aussinew[]
etdelete[]
) 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 (pourdelete
) ; 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
etr2
sont des vues sur la séquencedata
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).
- Il s'agit de nouveau d'une problématique de covariance ou de contratvariance.
- Ce fichier dans le dépôt Git montre des exemples de manipulations de sacs.