Note : Les réponses ont été données dans un ordre spécifique, mais comme de nombreux utilisateurs trient les réponses en fonction des votes, plutôt que de l'heure à laquelle elles ont été données, voici un index des réponses dans l'ordre où elles ont le plus de sens :
Note : Ceci est destiné à être une entrée de la FAQ C++ de Stack Overflow. Si vous souhaitez critiquer l'idée de fournir une FAQ sous cette forme, alors le message sur meta qui est à l'origine de tout ceci serait l'endroit idéal pour le faire. Les réponses à cette question sont suivies dans le salon de discussion C++, où l'idée de la FAQ a été lancée en premier lieu, donc votre réponse a de grandes chances d'être lue par ceux qui ont eu l'idée.)_
La plupart du travail de surcharge des opérateurs est du code passe-partout. Ce n'est pas étonnant, puisque les opérateurs ne sont que du sucre syntaxique, leur travail réel pourrait être effectué par (et est souvent transféré vers) des fonctions simples. Mais il est important que ce code passe-partout soit correct. Si vous échouez, soit le code de votre opérateur ne compilera pas, soit le code de vos utilisateurs ne compilera pas, soit le code de vos utilisateurs se comportera de manière surprenante.
Il y a beaucoup de choses à dire sur l'affectation. Cependant, la plupart d'entre elles ont déjà été dites dans la [célèbre FAQ Copy-And-Swap de GMan] (https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom), donc je vais sauter la plupart d'entre elles ici, en listant seulement l'opérateur d'assignation parfait pour référence :
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Les opérateurs bitshift <<
et >>
, bien qu'ils soient toujours utilisés dans l'interfaçage matériel pour les fonctions de manipulation de bits qu'ils héritent du C, sont devenus plus courants en tant qu'opérateurs d'entrée et de sortie de flux surchargés dans la plupart des applications. Pour des conseils sur la surcharge en tant qu'opérateurs de manipulation de bits, voir la section ci-dessous sur les opérateurs arithmétiques binaires. Pour implémenter votre propre format personnalisé et votre logique d'analyse lorsque votre objet est utilisé avec iostreams, continuez.
Les opérateurs de flux, parmi les opérateurs les plus couramment surchargés, sont des opérateurs infixes binaires pour lesquels la syntaxe ne spécifie aucune restriction sur le fait qu'ils doivent être membres ou non-membres.
Puisqu'ils changent leur argument de gauche (ils modifient l'état du flux), ils devraient, selon les règles empiriques, être implémentés comme membres du type de leur opérande de gauche. Cependant, leurs opérandes de gauche sont des flux de la bibliothèque standard, et bien que la plupart des opérateurs de sortie et d'entrée de flux définis par la bibliothèque standard soient effectivement définis comme membres des classes de flux, lorsque vous implémentez des opérations de sortie et d'entrée pour vos propres types, vous ne pouvez pas modifier les types de flux de la bibliothèque standard. C'est pourquoi vous devez implémenter ces opérateurs pour vos propres types en tant que fonctions non membres.
Les formes canoniques de ces deux fonctions sont les suivantes :
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
Lors de l'implémentation de operator>>
, le paramétrage manuel de l'état du flux n'est nécessaire que lorsque la lecture elle-même a réussi, mais que le résultat n'est pas celui attendu.
L'opérateur d'appel de fonction, utilisé pour créer des objets fonctionnels, également connus sous le nom de foncteurs, doit être défini comme une fonction member, il a donc toujours l'argument implicite this
des fonctions membres. En dehors de cela, il peut être surchargé pour prendre un nombre quelconque d'arguments supplémentaires, y compris zéro.
Voici un exemple de syntaxe :
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Utilisation :
foo f;
int a = f("hello");
Dans toute la bibliothèque standard C++, les objets de fonction sont toujours copiés. Vos propres objets fonctionnels doivent donc être peu coûteux à copier. Si un objet fonction a absolument besoin d'utiliser des données dont la copie est coûteuse, il est préférable de stocker ces données ailleurs et de faire en sorte que l'objet fonction y fasse référence.
Les opérateurs de comparaison binaires infixes doivent, selon les règles empiriques, être implémentés comme des fonctions non membres1. La négation préfixe unaire !
devrait (selon les mêmes règles) être implémentée comme une fonction membre. (mais ce n'est généralement pas une bonne idée de la surcharger).
Les algorithmes (par exemple std::sort()
) et les types (par exemple std::map
) de la bibliothèque standard n'attendront toujours que la présence de operator<
. Cependant, les utilisateurs de votre type s'attendront aussi à ce que tous les autres opérateurs soient présents, donc si vous définissez operator<
, assurez-vous de suivre la troisième règle fondamentale de la surcharge des opérateurs et définissez aussi tous les autres opérateurs de comparaison booléens. La manière canonique de les implémenter est la suivante :
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
La chose importante à noter ici est que seuls deux de ces opérateurs font réellement quelque chose, les autres ne font que transmettre leurs arguments à l'un ou l'autre de ces deux opérateurs pour qu'ils fassent le travail réel.
La syntaxe de surcharge des autres opérateurs booléens binaires (||
, &&
) suit les règles des opérateurs de comparaison. Cependant, il est très peu probable que vous trouviez un cas d'utilisation raisonnable pour ces2.
1 Comme pour toutes les règles empiriques, il peut parfois y avoir des raisons d'enfreindre celle-ci aussi. Si c'est le cas, n'oubliez pas que l'opérande de gauche des opérateurs de comparaison binaire, qui pour les fonctions membres sera *this
, doit être const
, aussi. Ainsi, un opérateur de comparaison implémenté comme une fonction membre devrait avoir cette signature:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Notez le const
à la fin.)
2 Il faut noter que la version intégrée de ||
et &&
utilise une sémantique de raccourci. Alors que celles définies par l'utilisateur (parce qu'elles sont du sucre syntaxique pour les appels de méthode) n'utilisent pas de sémantique de raccourci. L'utilisateur s'attendra à ce que ces opérateurs aient une sémantique de raccourci, et leur code peut en dépendre, Il est donc fortement conseillé de ne JAMAIS les définir.
Les opérateurs unaires d'incrémentation et de décrémentation existent en version préfixe et postfixe. Pour les différencier, les variantes postfixes prennent un argument int supplémentaire. Si vous surchargez incrément ou décrément, assurez-vous de toujours implémenter les versions préfixe et postfixe. Voici l'implémentation canonique de increment, decrement suit les mêmes règles :
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Notez que la variante postfix est implémentée en termes de préfixe. Notez également que postfix fait une copie supplémentaire.2
La surcharge de moins et de plus unaires n'est pas très courante et il est probablement préférable de l'éviter. Si nécessaire, ils devraient probablement être surchargés en tant que fonctions membres.
2 Notez également que la variante postfixe fait plus de travail et est donc moins efficace à utiliser que la variante préfixe. C'est une bonne raison pour préférer généralement l'incrémentation préfixe à l'incrémentation postfixe. Alors que les compilateurs peuvent généralement optimiser le travail supplémentaire de l'incrément postfixe pour les types intégrés, ils peuvent ne pas être en mesure de faire la même chose pour les types définis par l'utilisateur (qui pourraient être quelque chose d'aussi innocent qu'un itérateur de liste). Une fois que vous avez pris l'habitude de faire i++
, il devient très difficile de se rappeler de faire ++i
à la place lorsque i
n'est pas d'un type intégré (de plus, vous auriez à changer le code lorsque vous changez de type), donc il vaut mieux prendre l'habitude de toujours utiliser l'incrémentation préfixe, à moins que postfixe ne soit explicitement nécessaire.
Pour les opérateurs arithmétiques binaires, n'oubliez pas d'obéir à la troisième règle de base de surcharge des opérateurs : Si vous fournissez +
, fournissez également +=
, si vous fournissez -
, n'omettez pas -=
, etc. Andrew Koenig aurait été le premier à observer que les opérateurs d'affectation composés peuvent être utilisés comme base pour leurs homologues non composés. Autrement dit, l'opérateur +
est implémenté en termes de +=
, -
est implémenté en termes de -=
, etc.
Selon nos règles empiriques, +
et ses compagnons devraient être des non-membres, tandis que leurs homologues d'affectation composés (+=
etc.), changeant leur argument de gauche, devraient être un membre. Voici un exemple de code pour +=
et +
; les autres opérateurs arithmétiques binaires doivent être implémentés de la même manière :
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
L'opérateur +=
renvoie son résultat par référence, tandis que l'opérateur +
renvoie une copie de son résultat. Bien sûr, retourner une référence est généralement plus efficace que retourner une copie, mais dans le cas de operator+
, il n'y a aucun moyen de contourner la copie. Lorsque vous écrivez a + b
, vous vous attendez à ce que le résultat soit une nouvelle valeur, c'est pourquoi operator+
doit retourner une nouvelle valeur.3
Notez également que operator+
prend son opérande gauche par copie plutôt que par référence const. La raison pour cela est la même que celle qui donne pour operator=
prenant son argument par copie.
Les opérateurs de manipulation de bits ~
&
|
^
<<
>>
devraient être implémentés de la même manière que les opérateurs arithmétiques. Cependant, (à l'exception de la surcharge de <<
et >>
pour la sortie et l'entrée) il y a très peu de cas d'utilisation raisonnable pour les surcharger.
3 Encore, la leçon à tirer de ceci est que a += b
est, en général, plus efficace que a + b
et devrait être préféré si possible.
L'opérateur d'indice de tableau est un opérateur binaire qui doit être implémenté comme un membre de la classe. Il est utilisé pour les types de type conteneur qui permettent d'accéder à leurs éléments de données par une clé. La forme canonique pour les fournir est la suivante :
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
À moins que vous ne souhaitiez que les utilisateurs de votre classe puissent modifier les éléments de données renvoyés par operator[]
(auquel cas vous pouvez omettre la variante non-const), vous devez toujours fournir les deux variantes de l'opérateur.
Si value_type est connu pour faire référence à un type intégré, il est préférable que la variante const de l'opérateur renvoie une copie plutôt qu'une référence const :
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Pour définir vos propres itérateurs ou pointeurs intelligents, vous devez surcharger l'opérateur de déréférencement préfixe unaire *
et l'opérateur d'accès membre pointeur infixe binaire ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
->
, si value_type
est de type class
(ou struct
ou union
), un autre operator->()
est appelé récursivement, jusqu'à ce qu'un operator->()
retourne une valeur de type non-class.
L'opérateur unaire adresse-de ne doit jamais être surchargé.
Pour operator->*()
, voir cette question. Il est rarement utilisé et donc rarement surchargé. En fait, même les itérateurs ne le surchargent pas.Aller à Opérateurs de conversion
En matière de surcharge d'opérateurs en C++, il existe trois règles de base à suivre. Comme pour toutes ces règles, il existe des exceptions. Il est arrivé que des personnes s'en écartent et que le résultat ne soit pas un mauvais code, mais de telles déviations positives sont rares. Au minimum, 99 des 100 déviations que j'ai vues étaient injustifiées. Cependant, cela aurait tout aussi bien pu être 999 sur 1000. Vous feriez donc mieux de vous en tenir aux règles suivantes.
Lorsque la signification d'un opérateur n'est pas manifestement claire et incontestée, il ne faut pas le surcharger._ Au lieu de cela, il faut fournir une fonction avec un nom bien choisi. _La première règle à suivre est de ne pas surcharger un opérateur. Fondamentalement, la première et principale règle pour la surcharge d'opérateurs, à son cœur même, dit : Ne le faites pas. Cela peut sembler étrange, car il y a beaucoup à savoir sur la surcharge d'opérateurs et donc beaucoup d'articles, de chapitres de livres et d'autres textes traitent de tout cela. Mais malgré cette évidence apparente, il n'y a qu'un nombre étonnamment faible de cas où la surcharge d'opérateurs est appropriée. La raison en est qu'il est en fait difficile de comprendre la sémantique derrière l'application d'un opérateur à moins que l'utilisation de cet opérateur dans le domaine d'application soit bien connue et incontestée. Contrairement à la croyance populaire, ce n'est pratiquement jamais le cas.
Toujours s'en tenir à la sémantique bien connue de l'opérateur.
Le C++ ne pose aucune limite à la sémantique des opérateurs surchargés. Votre compilateur acceptera volontiers un code qui implémente l'opérateur binaire +
pour soustraire de son opérande droit. Cependant, les utilisateurs d'un tel opérateur ne soupçonneront jamais l'expression a + b
de soustraire a
de b
. Bien sûr, cela suppose que la sémantique de l'opérateur dans le domaine d'application est incontestée.
Toujours fournir toutes les opérations d'un ensemble d'opérations liées.
Les opérateurs sont liés les uns aux autres et aux autres opérations. Si votre type supporte a + b
, les utilisateurs s'attendront à pouvoir appeler a += b
, aussi. S'il supporte l'incrément préfixe ++a
, ils s'attendront à ce que a++
fonctionne également. S'ils peuvent vérifier si a < b
, ils s'attendront très certainement à pouvoir également vérifier si a > b
. S'ils peuvent copier et construire votre type, ils s'attendent à ce que l'affectation fonctionne également.
Passez à [La décision entre membre et non-membre] (https://stackoverflow.com/questions/4421706/operator-overloading-in-c/4421729#4421729).
Vous ne pouvez pas modifier la signification des opérateurs pour les types intégrés en C++, les opérateurs ne peuvent être surchargés que pour les types définis par l'utilisateur1. C'est-à-dire qu'au moins un des opérandes doit être d'un type défini par l'utilisateur. Comme pour les autres fonctions surchargées, les opérateurs ne peuvent être surchargés qu'une seule fois pour un certain ensemble de paramètres.
Tous les opérateurs ne peuvent pas être surchargés en C++. Parmi les opérateurs qui ne peuvent pas être surchargés, on trouve : .
::
sizeof
typeid
.*
et le seul opérateur ternaire en C++, ?:
.
Les opérateurs qui peuvent être surchargés en C++ sont les suivants :
+
-
*
/
%
et +=
-=
*=
/=
%=
(tous les infixes binaires) ; +
-
(préfixe unaire) ; ++
--
(préfixe et postfixe unaire)&
|
^
<<
>
et &=
|=
^=
<=
>=
(tous les infixes binaires) ; ~
(préfixe unaire)==
!=
<
>
<=
>=
||
&&
(tous les infixes binaires) ; !
(préfixe unaire)new
new[]
delete
delete[]
=
[]
->`->*
,
(tout infixe binaire) ; *
&
(tout préfixe unaire) ()
(appel de fonction, infixe n-aire)Cependant, le fait que vous pouvez surcharger tous ces opérateurs ne signifie pas que vous deviez le faire. Voir les règles de base de la surcharge des opérateurs.
En C++, les opérateurs sont surchargés sous la forme de fonctions avec des noms spéciaux. Comme pour les autres fonctions, les opérateurs surchargés peuvent généralement être implémentés soit comme une fonction membre du type de leur opérande gauche, soit comme des fonctions non membres. Le fait que vous soyez libre de choisir ou obligé d'utiliser l'une ou l'autre dépend de plusieurs critères.2 Un opérateur unaire @
3, appliqué à un objet x, est invoqué soit comme operator@(x)
, soit comme x.operator@()
. Un opérateur infixe binaire @
, appliqué aux objets x
et y
, est invoqué soit comme operator@(x,y)
, soit comme x.operator@(y)
.4.
Les opérateurs qui sont implémentés comme des fonctions non membres sont parfois amis du type de leur opérande.
1 Le terme "défini par l'utilisateur" pourrait être légèrement trompeur. Le C++ fait la distinction entre les types intégrés et les types définis par l'utilisateur. Aux premiers appartiennent par exemple int, char et double ; aux seconds appartiennent tous les types struct, class, union et enum, y compris ceux de la bibliothèque standard, même s'ils ne sont pas, en tant que tels, définis par les utilisateurs.
2 Ce sujet est traité dans une partie ultérieure de cette FAQ.
3 Le @
n'est pas un opérateur valide en C++, c'est pourquoi je l'utilise comme substitut.
4 Le seul opérateur ternaire en C++ ne peut pas être surchargé et le seul opérateur n-aire doit toujours être implémenté comme une fonction membre.
Passez à [Les trois règles de base de la surcharge d'opérateurs en C++] (https://stackoverflow.com/questions/4421706/operator-overloading-in-c/4421708#4421708).