operator overloading
Personnalise les opérateurs C++ pour les opérandes de types définis par l'utilisateur.
Syntaxe
Fonctions d'opérateur sont des fonctions avec des noms de fonction spéciaux :
operator
op
|
(1) | ||||||||
operator
new
operator
new []
|
(2) | ||||||||
operator
delete
operator
delete []
|
(3) | ||||||||
operator
co_await
|
(4) | (depuis C++20) | |||||||
| op | - | l'un des opérateurs suivants : + - * / % ^ & | ~ ! = < > + = - = * = / = % = ^ = & = | = << >> >>= <<= == ! = <= >= <=> (depuis C++20) && || ++ -- , - > * - > ( ) [ ] |
Les comportements des opérateurs non-ponctuation sont décrits dans leurs propres pages respectives. Sauf indication contraire, la description restante dans cette page ne s'applique pas à ces fonctions.
Explication
Lorsqu'un opérateur apparaît dans une expression , et qu'au moins un de ses opérandes a un type classe ou un type énumération , alors la résolution de surcharge est utilisée pour déterminer la fonction définie par l'utilisateur à appeler parmi toutes les fonctions dont les signatures correspondent à ce qui suit :
| Expression | En tant que fonction membre | En tant que fonction non-membre | Exemple |
|---|---|---|---|
| @a | (a).operator@ ( ) | operator@ (a) | ! std:: cin appelle std:: cin . operator ! ( ) |
| a@b | (a).operator@ (b) | operator@ (a, b) | std:: cout << 42 appelle std:: cout . operator << ( 42 ) |
| a=b | (a).operator= (b) | ne peut pas être non-membre | Étant donné std:: string s ; , s = "abc" ; appelle s. operator = ( "abc" ) |
| a(b...) | (a).operator()(b...) | ne peut pas être non-membre | Étant donné std:: random_device r ; , auto n = r ( ) ; appelle r. operator ( ) ( ) |
| a[b...] | (a).operator[](b...) | ne peut pas être non-membre | Étant donné std:: map < int , int > m ; , m [ 1 ] = 2 ; appelle m. operator [ ] ( 1 ) |
| a-> | (a).operator->( ) | ne peut pas être non-membre | Étant donné std:: unique_ptr < S > p ; , p - > bar ( ) appelle p. operator - > ( ) |
| a@ | (a).operator@ (0) | operator@ (a, 0) | Étant donné std:: vector < int > :: iterator i ; , i ++ appelle i. operator ++ ( 0 ) |
|
Dans ce tableau,
|
|||
|
De plus, pour les opérateurs de comparaison == , ! = , < , > , <= , >= , <=> , la résolution de surcharge considère également les candidats réécrits operator == ou operator <=> . |
(depuis C++20) |
Les opérateurs surchargés (mais pas les opérateurs intégrés) peuvent être appelés en utilisant la notation de fonction :
std::string str = "Bonjour, "; str.operator+=("monde"); // identique à str += "monde"; operator<<(operator<<(std::cout, str), '\n'); // identique à std::cout << str << '\n'; // (depuis C++17) sauf pour le séquencement
Opérateurs surchargés statiquesLes opérateurs surchargés qui sont des fonctions membres peuvent être déclarés static . Cependant, ceci est uniquement autorisé pour operator ( ) et operator [ ] . Ces opérateurs peuvent être appelés en utilisant la notation de fonction. Cependant, lorsque ces opérateurs apparaissent dans des expressions, ils nécessitent toujours un objet de type classe. struct SwapThem { template<typename T> static void operator()(T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } template<typename T> static void operator[](T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } }; inline constexpr SwapThem swap_them{}; void foo() { int a = 1, b = 2; swap_them(a, b); // OK swap_them[a, b]; // OK SwapThem{}(a, b); // OK SwapThem{}[a, b]; // OK SwapThem::operator()(a, b); // OK SwapThem::operator[](a, b); // OK SwapThem(a, b); // error, invalid construction SwapThem[a, b]; // error } |
(depuis C++23) |
Restrictions
- Une fonction d'opérateur doit avoir au moins un paramètre de fonction ou un paramètre d'objet implicite dont le type est une classe, une référence à une classe, une énumération, ou une référence à une énumération.
-
Les opérateurs
::(résolution de portée),.(accès membre),.*(accès membre via pointeur sur membre), et?:(conditionnel ternaire) ne peuvent pas être surchargés. -
De nouveaux opérateurs tels que
**,<>, ou&|ne peuvent pas être créés. - Il n'est pas possible de modifier la priorité, le groupement ou le nombre d'opérandes des opérateurs.
-
La surcharge de l'opérateur
->doit soit retourner un pointeur brut, soit retourner un objet (par référence ou par valeur) pour lequel l'opérateur->est à son tour surchargé. -
Les surcharges des opérateurs
&&et||perdent l'évaluation en court-circuit.
|
(jusqu'à C++17) |
Implémentations canoniques
En dehors des restrictions ci-dessus, le langage n'impose aucune autre contrainte sur ce que font les opérateurs surchargés, ou sur le type de retour (il ne participe pas à la résolution de surcharge), mais en général, les opérateurs surchargés sont censés se comporter de manière aussi similaire que possible aux opérateurs intégrés : operator + est censé additionner, plutôt que multiplier ses arguments, operator = est censé assigner, etc. Les opérateurs associés sont censés se comporter de manière similaire ( operator + et operator + = effectuent la même opération de type addition). Les types de retour sont limités par les expressions dans lesquelles l'opérateur est censé être utilisé : par exemple, les opérateurs d'assignement retournent par référence pour permettre d'écrire a = b = c = d , car les opérateurs intégrés le permettent.
Les opérateurs couramment surchargés ont les formes typiques et canoniques suivantes : [1]
Opérateur d'affectation
L'opérateur d'assignation operator = possède des propriétés spéciales : voir l'assignation de copie et l'assignation de déplacement pour plus de détails.
L'opérateur canonique de copie par assignation est censé être sûr en cas d'auto-assignation , et retourner le lhs par référence :
// assignation de copie T& operator=(const T& other) { // Protection contre l'auto-assignation if (this == &other) return *this; // suppose que *this gère une ressource réutilisable, telle qu'un tampon alloué sur le tas mArray if (size != other.size) // la ressource dans *this ne peut pas être réutilisée { temp = new int[other.size]; // alloue la ressource, si exception, ne fait rien delete[] mArray; // libère la ressource dans *this mArray = temp; size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); return *this; }
|
L'assignation de déplacement canonique est censée laisser l'objet source dans un état valide (c'est-à-dire un état où les invariants de classe sont préservés), et soit ne rien faire soit au moins laisser l'objet dans un état valide en cas d'auto-assignation, et retourner le lhs par référence non-const, et être noexcept : // move assignment T& operator=(T&& other) noexcept { // Guard self assignment if (this == &other) return *this; // delete[]/size=0 would also be ok delete[] mArray; // release resource in *this mArray = std::exchange(other.mArray, nullptr); // leave other in valid state size = std::exchange(other.size, 0); return *this; } |
(depuis C++11) |
Dans les situations où l'affectation par copie ne peut pas bénéficier de la réutilisation des ressources (elle ne gère pas de tableau alloué sur le tas et n'a pas de membre (éventuellement transitif) qui le fait, tel qu'un membre std::vector ou std::string ), il existe un raccourci pratique populaire : l'opérateur d'affectation par copie-et-échange, qui prend son paramètre par valeur (fonctionnant ainsi à la fois comme affectation par copie et par déplacement selon la catégorie de valeur de l'argument), échange avec le paramètre, et laisse le destructeur le nettoyer.
// assignation par copie (idiome copy-and-swap) T& T::operator=(T other) noexcept // appelle le constructeur de copie ou de déplacement pour construire other { std::swap(size, other.size); // échange les ressources entre *this et other std::swap(mArray, other.mArray); return *this; } // le destructeur de other est appelé pour libérer les ressources précédemment gérées par *this
Ce formulaire fournit automatiquement strong exception guarantee , mais interdit la réutilisation des ressources.
Extraction et insertion de flux
Les surcharges de
operator>>
et
operator<<
qui prennent un
std::
istream
&
ou un
std::
ostream
&
comme argument gauche sont appelées opérateurs d'insertion et d'extraction. Comme elles prennent le type défini par l'utilisateur comme argument droit (
b
dans
a @ b
), elles doivent être implémentées comme fonctions non-membres.
std::ostream& operator<<(std::ostream& os, const T& obj) { // écrire obj dans le flux return os; } std::istream& operator>>(std::istream& is, T& obj) { // lire obj depuis le flux if (/* T n'a pas pu être construit */) is.setstate(std::ios::failbit); return is; }
Ces opérateurs sont parfois implémentés comme friend functions .
Opérateur d'appel de fonction
Lorsqu'une classe définie par l'utilisateur surcharge l'opérateur d'appel de fonction operator ( ) , elle devient un type FunctionObject .
Un objet de ce type peut être utilisé dans une expression d'appel de fonction :
// Un objet de ce type représente une fonction linéaire d'une variable a * x + b. struct Linear { double a, b; double operator()(double x) const { return a * x + b; } }; int main() { Linear f{2, 1}; // Représente la fonction 2x + 1. Linear g{-1, 0}; // Représente la fonction -x. // f et g sont des objets qui peuvent être utilisés comme une fonction. double f_0 = f(0); double f_1 = f(1); double g_0 = g(0); }
De nombreuses algorithmes standards de la bibliothèque acceptent des FunctionObject s pour personnaliser leur comportement. Il n'existe pas de formes canoniques particulièrement notables pour l' operator ( ) , mais pour illustrer son utilisation :
#include <algorithm> #include <iostream> #include <vector> struct Sum { int sum = 0; void operator()(int n) { sum += n; } }; int main() { std::vector<int> v = {1, 2, 3, 4, 5}; Sum s = std::for_each(v.begin(), v.end(), Sum()); std::cout << "The sum is " << s.sum << '\n'; }
Sortie :
The sum is 15
Incrément et décrément
Lorsque l'opérateur d'incrémentation ou de décrémentation postfixé apparaît dans une expression, la fonction définie par l'utilisateur correspondante ( operator ++ ou operator -- ) est appelée avec un argument entier 0 . Typiquement, elle est déclarée comme T operator ++ ( int ) ou T operator -- ( int ) , où l'argument est ignoré. Les opérateurs d'incrémentation et de décrémentation postfixés sont généralement implémentés en fonction des versions préfixées :
struct X { // incrémentation préfixe X& operator++() { // l'incrémentation réelle a lieu ici return *this; // retourne la nouvelle valeur par référence } // incrémentation postfixe X operator++(int) { X old = *this; // copie l'ancienne valeur operator++(); // incrémentation préfixe return old; // retourne l'ancienne valeur } // décrémentation préfixe X& operator--() { // la décrémentation réelle a lieu ici return *this; // retourne la nouvelle valeur par référence } // décrémentation postfixe X operator--(int) { X old = *this; // copie l'ancienne valeur operator--(); // décrémentation préfixe return old; // retourne l'ancienne valeur } };
Bien que les implémentations canoniques des opérateurs de pré-incrément et de pré-décrément retournent par référence, comme pour toute surcharge d'opérateur, le type de retour est défini par l'utilisateur ; par exemple, les surcharges de ces opérateurs pour std::atomic retournent par valeur.
Opérateurs arithmétiques binaires
Les opérateurs binaires sont généralement implémentés en tant que non-membres pour préserver la symétrie (par exemple, lors de l'addition d'un nombre complexe et d'un entier, si operator + est une fonction membre du type complexe, alors seul complex + integer serait compilé, et non integer + complex ). Puisque pour chaque opérateur arithmétique binaire il existe un opérateur d'affectation composé correspondant, les formes canoniques des opérateurs binaires sont implémentées en utilisant leurs affectations composées :
class X { public: X& operator+=(const X& rhs) // assignation composée (n'a pas besoin d'être membre, { // mais l'est souvent, pour modifier les membres privés) /* l'addition de rhs à *this a lieu ici */ return *this; // retourne le résultat par référence } // les fonctions amies définies dans le corps de la classe sont inline et cachées de la recherche non-ADL friend X operator+(X lhs, // passer lhs par valeur aide à optimiser les chaînes a+b+c const X& rhs) // sinon, les deux paramètres peuvent être des références constantes { lhs += rhs; // réutilise l'assignation composée return lhs; // retourne le résultat par valeur (utilise le constructeur de déplacement) } };
Opérateurs de comparaison
Les algorithmes de la bibliothèque standard tels que std::sort et les conteneurs tels que std::set s'attendent à ce que operator < soit défini, par défaut, pour les types fournis par l'utilisateur, et s'attendent à ce qu'il implémente un ordre strict faible (satisfaisant ainsi les exigences Compare ). Une manière idiomatique d'implémenter un ordre strict faible pour une structure est d'utiliser la comparaison lexicographique fournie par std::tie :
struct Record { std::string name; unsigned int floor; double weight; friend bool operator<(const Record& l, const Record& r) { return std::tie(l.name, l.floor, l.weight) < std::tie(r.name, r.floor, r.weight); // conserver le même ordre } };
En général, une fois que operator < est fourni, les autres opérateurs relationnels sont implémentés en fonction de operator < .
inline bool operator< (const X& lhs, const X& rhs) { /* effectuer la comparaison réelle */ } inline bool operator> (const X& lhs, const X& rhs) { return rhs < lhs; } inline bool operator<=(const X& lhs, const X& rhs) { return !(lhs > rhs); } inline bool operator>=(const X& lhs, const X& rhs) { return !(lhs < rhs); }
De même, l'opérateur d'inégalité est généralement implémenté en fonction de operator == :
inline bool operator==(const X& lhs, const X& rhs) { /* effectuer la comparaison réelle */ } inline bool operator!=(const X& lhs, const X& rhs) { return !(lhs == rhs); }
Lorsqu'une comparaison à trois voies (telle que std::memcmp ou std::string::compare ) est fournie, les six opérateurs de comparaison binaire peuvent être exprimés à travers celle-ci :
inline bool operator==(const X& lhs, const X& rhs) { return cmp(lhs,rhs) == 0; } inline bool operator!=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) != 0; } inline bool operator< (const X& lhs, const X& rhs) { return cmp(lhs,rhs) < 0; } inline bool operator> (const X& lhs, const X& rhs) { return cmp(lhs,rhs) > 0; } inline bool operator<=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) <= 0; } inline bool operator>=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) >= 0; }
Opérateur d'indice de tableau
Les classes définies par l'utilisateur qui fournissent un accès de type tableau permettant à la fois la lecture et l'écriture définissent généralement deux surcharges pour operator [ ] : des variantes const et non-const :
struct T { value_t& operator[](std::size_t idx) { return mVector[idx]; } const value_t& operator[](std::size_t idx) const { return mVector[idx]; } };
|
Alternativement, ils peuvent être exprimés comme une fonction membre template unique en utilisant un paramètre objet explicite : struct T { decltype(auto) operator[](this auto& self, std::size_t idx) { return self.mVector[idx]; } }; |
(depuis C++23) |
Si le type de valeur est connu comme étant un type scalaire, la variante const doit retourner par valeur.
Lorsque l'accès direct aux éléments du conteneur n'est pas souhaité ou possible, ou pour distinguer l'utilisation de lvalue c [ i ] = v ; et de rvalue v = c [ i ] ; , operator [ ] peut renvoyer un proxy. Voir par exemple std::bitset::operator[] .
|
operator [ ] ne peut prendre qu'un seul indice. Afin de fournir une sémantique d'accès aux tableaux multidimensionnels, par exemple pour implémenter un accès à un tableau 3D a [ i ] [ j ] [ k ] = x ; , operator [ ] doit renvoyer une référence à un plan 2D, qui doit avoir son propre operator [ ] qui renvoie une référence à une ligne 1D, qui doit avoir operator [ ] qui renvoie une référence à l'élément. Pour éviter cette complexité, certaines bibliothèques optent pour la surcharge de operator ( ) à la place, de sorte que les expressions d'accès 3D aient la syntaxe de type Fortran a ( i, j, k ) = x ; . |
(jusqu'en C++23) |
|
operator [ ] peut prendre n'importe quel nombre d'indices. Par exemple, un operator [ ] d'une classe de tableau 3D déclarée comme T & operator [ ] ( std:: size_t x, std:: size_t y, std:: size_t z ) ; peut accéder directement aux éléments.
Exécuter ce code
#include <array> #include <cassert> #include <iostream> template<typename T, std::size_t Z, std::size_t Y, std::size_t X> struct Array3d { std::array<T, X * Y * Z> m{}; constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23 { assert(x < X and y < Y and z < Z); return m[z * Y * X + y * X + x]; } }; int main() { Array3d<int, 4, 3, 2> v; v[3, 2, 1] = 42; std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << '\n'; } Sortie : v[3, 2, 1] = 42 |
(depuis C++23) |
Opérateurs arithmétiques bit à bit
Les classes et énumérations définies par l'utilisateur qui implémentent les exigences de BitmaskType doivent surcharger les opérateurs arithmétiques bit à bit operator & , operator | , operator ^ , operator~ , operator & = , operator | = , et operator ^ = , et peuvent optionnellement surcharger les opérateurs de décalage operator << operator >> , operator >>= , et operator <<= . Les implémentations canoniques suivent généralement le modèle décrit ci-dessus pour les opérateurs arithmétiques binaires.
Opérateur de négation booléenne
|
L'opérateur operator ! est couramment surchargé par les classes définies par l'utilisateur destinées à être utilisées dans des contextes booléens. Ces classes fournissent également une fonction de conversion définie par l'utilisateur vers le type booléen (voir std::basic_ios pour l'exemple de la bibliothèque standard), et le comportement attendu de operator ! est de renvoyer la valeur opposée de operator bool . |
(jusqu'à C++11) |
|
Étant donné que l'opérateur intégré ! effectue une conversion contextuelle en bool , les classes définies par l'utilisateur destinées à être utilisées dans des contextes booléens peuvent uniquement fournir operator bool et n'ont pas besoin de surcharger operator ! . |
(depuis C++11) |
Opérateurs rarement surchargés
Les opérateurs suivants sont rarement surchargés :
-
L'opérateur d'adresse,
operator
&
. Si l'opérateur unaire & est appliqué à une lvalue de type incomplet et que le type complet déclare un
operator
&
surchargé, il n'est pas spécifié si l'opérateur a la signification intégrée ou si la fonction opérateur est appelée. Comme cet opérateur peut être surchargé, les bibliothèques génériques utilisent
std::addressof
pour obtenir les adresses des objets de types définis par l'utilisateur. L'exemple le plus connu d'un
operator
&
surchargé canonique est la classe Microsoft
CComPtrBase. Un exemple d'utilisation de cet opérateur dans un EDSL peut être trouvé dans boost.spirit . - Les opérateurs logiques booléens, operator && et operator || . Contrairement aux versions intégrées, les surcharges ne peuvent pas implémenter l'évaluation en court-circuit. Également contrairement aux versions intégrées, elles ne séquencent pas leur opérande gauche avant l'opérande droit. (jusqu'à C++17) Dans la bibliothèque standard, ces opérateurs ne sont surchargés que pour std::valarray .
- L'opérateur virgule, operator, . Contrairement à la version intégrée, les surcharges ne séquencent pas leur opérande gauche avant l'opérande droit. (jusqu'à C++17) Comme cet opérateur peut être surchargé, les bibliothèques génériques utilisent des expressions telles que a, void ( ) , b au lieu de a, b pour séquencer l'exécution d'expressions de types définis par l'utilisateur. La bibliothèque boost utilise operator, dans boost.assign , boost.spirit , et d'autres bibliothèques. La bibliothèque d'accès aux bases de données SOCI surcharge également operator, .
- L'opérateur d'accès membre via pointeur sur membre operator - > * . Il n'y a pas d'inconvénients spécifiques à la surcharge de cet opérateur, mais il est rarement utilisé en pratique. Il a été suggéré qu'il pourrait faire partie d'une interface de pointeur intelligent , et est en effet utilisé dans cette capacité par les acteurs dans boost.phoenix . Il est plus courant dans les EDSL tels que cpp.react .
Notes
| Macro de test de fonctionnalité | Valeur | Std | Fonctionnalité |
|---|---|---|---|
__cpp_static_call_operator
|
202207L
|
(C++23) | static operator ( ) |
__cpp_multidimensional_subscript
|
202211L
|
(C++23) | static operator [ ] |
Mots-clés
Exemple
#include <iostream> class Fraction { // ou std::gcd de C++17 constexpr int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: constexpr Fraction(int n, int d = 1) : n(n / gcd(n, d)), d(d / gcd(n, d)) {} constexpr int num() const { return n; } constexpr int den() const { return d; } constexpr Fraction& operator*=(const Fraction& rhs) { int new_n = n * rhs.n / gcd(n * rhs.n, d * rhs.d); d = d * rhs.d / gcd(n * rhs.n, d * rhs.d); n = new_n; return *this; } }; std::ostream& operator<<(std::ostream& out, const Fraction& f) { return out << f.num() << '/' << f.den(); } constexpr bool operator==(const Fraction& lhs, const Fraction& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } constexpr bool operator!=(const Fraction& lhs, const Fraction& rhs) { return !(lhs == rhs); } constexpr Fraction operator*(Fraction lhs, const Fraction& rhs) { return lhs *= rhs; } int main() { constexpr Fraction f1{3, 8}, f2{1, 2}, f3{10, 2}; std::cout << f1 << " * " << f2 << " = " << f1 * f2 << '\n' << f2 << " * " << f3 << " = " << f2 * f3 << '\n' << 2 << " * " << f1 << " = " << 2 * f1 << '\n'; static_assert(f3 == f2 * 10); }
Sortie :
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
Rapports de défauts
Les rapports de défauts modifiant le comportement suivants ont été appliqués rétroactivement aux normes C++ précédemment publiées.
| DR | Appliqué à | Comportement publié | Comportement corrigé |
|---|---|---|---|
| CWG 1481 | C++98 |
l'opérateur de pré-incrément non-membre ne pouvait avoir qu'un paramètre
de type classe, type énumération, ou un type référence vers ces types |
aucune exigence de type |
| CWG 2931 | C++23 |
les fonctions opérateur membres à objet explicite ne pouvaient avoir aucun paramètre
de type classe, type énumération, ou un type référence vers ces types |
interdit |
Voir aussi
| Opérateurs courants | ||||||
|---|---|---|---|---|---|---|
| affectation |
incrémentation
décrémentation |
arithmétique | logique | comparaison |
accès
membre |
autres |
|
a
=
b
|
++
a
|
+
a
|
!
a
|
a
==
b
|
a
[
...
]
|
appel de fonction
a ( ... ) |
|
virgule
a, b |
||||||
|
conditionnel
a ? b : c |
||||||
| Opérateurs spéciaux | ||||||
|
static_cast
convertit un type en un autre type apparenté
|
||||||
Liens externes
|