Opérateurs communs à surcharger
La plupart du travail des opérateurs de surcharge est le code de la chaudière. 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 transmis à) des fonctions simples. Mais il est important que vous obteniez ce bon code de plaque de chaudière. Si vous échouez, le code de votre opérateur ne se compilera pas ou le code de vos utilisateurs ne se compilera pas ou le code de vos utilisateurs se comportera de manière surprenante.
Opérateur d'assignation
Il y a beaucoup à dire sur l'affectation. Cependant, la plupart d'entre eux ont déjà été mentionnés dans la célèbre FAQ de copie et d'échange de GMan , donc je vais en sauter la plupart ici, en ne listant que l'opérateur d'affectation parfait pour référence:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Opérateurs Bitshift (utilisés pour les E / S de flux)
Les opérateurs de décalage de bits <<
et >>
, bien qu'ils soient toujours utilisés dans l'interface matérielle pour les fonctions de manipulation de bits qu'ils héritent de 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 informations sur la surcharge des opérateurs de manipulation de bits, consultez la section ci-dessous sur les opérateurs arithmétiques binaires. Pour implémenter votre propre format personnalisé et logique d'analyse lorsque votre objet est utilisé avec des iostreams, continuez.
Les opérateurs de flux, parmi les opérateurs les plus fréquemment surchargés, sont des opérateurs d'infixes binaires pour lesquels la syntaxe ne spécifie aucune restriction quant à savoir s'ils doivent être membres ou non membres. Puisqu'ils changent leur argument gauche (ils modifient l'état du flux), ils devraient, selon les règles de base, être implémentés en tant que membres du type de leur opérande 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 en effet 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 peut 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 des deux 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 operator>>
, la définition manuelle de l'état du flux n'est nécessaire que lorsque la lecture elle-même a réussi, mais le résultat n'est pas celui attendu.
Opérateur d'appel de fonction
L'opérateur d'appel de fonction, utilisé pour créer des objets de fonction, également appelés foncteurs, doit être défini comme une fonction membre , de sorte qu'il a toujours l' this
argument implicite des fonctions membres. En dehors de cela, il peut être surchargé pour accepter n'importe quel nombre 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) {
// ...
}
};
Usage:
foo f;
int a = f("hello");
Dans toute la bibliothèque standard C ++, les objets fonction sont toujours copiés. Vos propres objets de fonction devraient donc être bon marché à copier. Si un objet fonction doit absolument utiliser des données dont la copie est coûteuse, il est préférable de stocker ces données ailleurs et de se référer à l'objet fonction.
Opérateurs de comparaison
Les opérateurs de comparaison d'infixes binaires doivent, selon les règles de base, être implémentés en tant que fonctions non membres 1 . La négation du préfixe unaire !
doit (selon les mêmes règles) être implémentée en tant que fonction membre. (mais ce n'est généralement pas une bonne idée de le surcharger.)
Les algorithmes std::sort()
et les types standard de la bibliothèque (par exemple std::map
) s'attendent toujours operator<
à être présents. Cependant, les utilisateurs de votre type s'attendront à ce que tous les autres opérateurs soient également présents , donc si vous définissez operator<
, assurez-vous de suivre la troisième règle fondamentale de surcharge des opérateurs et définissez également 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 pour faire le travail réel.
La syntaxe de surcharge des opérateurs booléens binaires restants ( ||
, &&
) 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 ces 2 .
1 Comme pour toutes les règles empiriques, il peut parfois y avoir des raisons de rompre celle-ci également. Si c'est le cas, n'oubliez pas que l'opérande de gauche des opérateurs de comparaison binaires, qui le sera pour les fonctions membres *this
, doit également l'être const
. Ainsi, un opérateur de comparaison implémenté en tant que fonction membre devrait avoir cette signature:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Notez le const
à la fin.)
2 Il convient de noter que la version intégrée de ||
et &&
utilise la sémantique des raccourcis. Bien que ceux définis par l'utilisateur (car ils 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.
Opérateurs arithmétiques
Opérateurs arithmétiques unaires
Les opérateurs d'incrémentation et de décrémentation unaires sont proposés à la fois en préfixe et en postfixe. Pour se différencier, les variantes postfixes prennent un argument int factice supplémentaire. Si vous surchargez l'incrémentation ou la décrémentation, assurez-vous de toujours implémenter les versions préfixée et postfixée. Voici l'implémentation canonique de l'incrémentation, la décrémentation 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 unaire moins et plus n'est pas très courante et probablement mieux évitée. Si nécessaire, ils devraient probablement être surchargés en tant que fonctions membres.
2 Notez également que la variante postfix fait plus de travail et est donc moins efficace à utiliser que la variante préfixe. C'est une bonne raison de préférer généralement l'incrément de préfixe à l'incrément de postfix. Bien que les compilateurs puissent généralement optimiser le travail supplémentaire d'incrémentation postfixe pour les types intégrés, ils pourraient ne pas être en mesure de faire de même pour les types définis par l'utilisateur (ce qui pourrait être quelque chose d'aussi innocent qu'un itérateur de liste). Une fois que vous vous êtes habitué à le faire i++
, il devient très difficile de se rappeler de le faire à la ++i
place lorsqu'il i
n'est pas de type intégré (en plus, vous devrez changer le code lors du changement de type), il est donc préférable de prendre l'habitude de toujours en utilisant l'incrément de préfixe, sauf si le suffixe est explicitement nécessaire.
Opérateurs arithmétiques binaires
Pour les opérateurs arithmétiques binaires, n'oubliez pas d'obéir à la troisième surcharge de base de l'opérateur de règle: si vous fournissez +
, fournissez également +=
, si vous fournissez -
, n'omettez pas -=
, etc. Andrew Koenig aurait été le premier à observer que l'affectation composée les opérateurs peuvent être utilisés comme base pour leurs homologues non composés. Autrement dit, l'opérateur +
est mis en œuvre en termes de +=
, -
est mis en œuvre en termes de -=
etc.
Selon nos règles empiriques, +
et ses compagnons doivent être non-membres, tandis que leurs homologues d'affectation composée ( +=
etc.), en changeant leur argument de gauche, doivent être membres. Voici l'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;
}
operator+=
renvoie son résultat par référence, tandis que operator+
renvoie une copie de son résultat. Bien sûr, le renvoi d'une référence est généralement plus efficace que le renvoi d'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 renvoyer 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 en est la même que la raison invoquée pour operator=
prendre son argument par copie.
Les opérateurs de manipulation de bits ~
&
|
^
<<
>>
doivent être implémentés de la même manière que les opérateurs arithmétiques. Cependant (à l'exception de la surcharge <<
et >>
de la sortie et de l'entrée), il existe très peu de cas d'utilisation raisonnables pour les surcharger.
3 Encore une fois, la leçon à tirer de cela est a += b
, en général, plus efficace que a + b
et devrait être préférée si possible.
Indice de tableau
L'opérateur d'indice de tableau est un opérateur binaire qui doit être implémenté en tant que membre de 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 de fournir ces informations est la suivante:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
À moins que vous ne vouliez pas 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é, la variante const de l'opérateur devrait mieux renvoyer 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;
// ...
};
Opérateurs pour les types de type pointeur
Pour définir vos propres itérateurs ou pointeurs intelligents, vous devez surcharger l'opérateur de déréférence de préfixe unaire *
et l'opérateur d'accès au membre du pointeur d'infixe binaire ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Notez que ceux-ci, aussi, auront presque toujours besoin à la fois d'une version const et d'une version non const. Pour l' ->
opérateur, si value_type
est de type class
(ou struct
ou union
), un autre operator->()
est appelé récursivement, jusqu'à ce que an operator->()
renvoie une valeur de type non-classe.
L'opérateur d'adresse unaire 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.
Continuez vers les opérateurs de conversion