Quelles sont les règles et idiomes de base pour la surcharge d'opérateur?

2145

Remarque: 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 du moment où elles ont été données, voici un index des réponses dans l'ordre dans lequel elles ont le plus de sens:

(Remarque: Ceci est censé être une entrée de la FAQ C ++ de Stack Overflow . Si vous voulez critiquer l'idée de fournir une FAQ sous cette forme, alors la publication sur la méta qui a commencé tout cela serait l'endroit pour le faire. Réponses à cette question est surveillée dans le salon de discussion C ++ , où l'idée de FAQ a commencé en premier lieu, donc votre réponse est très susceptible d'être lue par ceux qui ont eu l'idée.)

sbi
la source
63
Si nous continuons avec la balise C ++ - FAQ, voici comment les entrées doivent être formatées.
John Dibling
J'ai écrit une courte série d'articles pour la communauté C ++ allemande sur la surcharge des opérateurs: Partie 1: la surcharge des opérateurs en C ++ couvre la sémantique, l'utilisation typique et les spécialités de tous les opérateurs. Il y a des chevauchements avec vos réponses ici, néanmoins il y a quelques informations supplémentaires. Les parties 2 et 3 proposent un didacticiel sur l'utilisation des opérateurs Boost. Souhaitez-vous que je les traduise et les ajoute comme réponses?
Arne Mertz
Oh, et une traduction en anglais est également disponible: les bases et la pratique courante
Arne Mertz

Réponses:

1044

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' thisargument 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 ++iplace lorsqu'il in'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 + bet 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_typeest de type class(ou structou 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

sbi
la source
89
operator->()est en fait extrêmement bizarre. Il n'est pas nécessaire de renvoyer un value_type*- en fait, il peut renvoyer un autre type de classe, à condition que ce type de classe ait unoperator->() , qui sera ensuite appelé par la suite. Cet appel récursif de operator->()s se poursuit jusqu'à ce qu'un value_type*type de retour se produise. La démence! :)
j_random_hacker
2
Ce n'est pas exactement une question d'efficacité. Il s'agit de ne pas pouvoir le faire de manière traditionnelle-idiomatique dans un (très) petit nombre de cas: lorsque la définition des deux opérandes doit rester inchangée pendant que nous calculons le résultat. Et comme je l'ai dit, il y a deux exemples classiques: la multiplication des matrices et la multiplication des polynômes. Nous pourrions définir *en termes de *=mais ce serait gênant car l'une des premières opérations de *=serait de créer un nouvel objet, résultat du calcul. Ensuite, après la boucle for-ijk, nous échangerions cet objet temporaire avec *this. c'est à dire. 1. copie, 2. opérateur *, 3. échange
Luc Hermitte
6
Je ne suis pas d'accord avec les versions const / non const de vos opérateurs de type pointeur, par exemple `const value_type & operator * () const;` - ce serait comme avoir un T* constretour à un const T&déréférencement, ce qui n'est pas le cas. Ou en d'autres termes: un pointeur const n'implique pas une pointe const. En fait, il n'est pas trivial d'imiter T const *- ce qui est la raison de tout le const_iteratorcontenu de la bibliothèque standard. Conclusion: la signature devrait êtrereference_type operator*() const; pointer_type operator->() const
Arne Mertz
6
Un commentaire: L'implémentation d'opérateurs arithmétiques binaires suggérée n'est pas aussi efficace qu'elle peut l'être. Remarque sur la simulation de l'en-tête des opérateurs Se Boost: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Une copie de plus peut être évitée si vous utilisez une copie locale du premier paramètre, faites + = et retournez le copie locale. Cela permet l'optimisation NRVO.
Manu343726
3
Comme je l'ai mentionné dans le chat, L <= Rpeut également être exprimé comme !(R < L)au lieu de !(L > R). Pourrait économiser une couche supplémentaire d'inlining dans les expressions difficiles à optimiser (et c'est aussi la façon dont Boost.Operators l'implémente).
TemplateRex
494

Les trois règles de base de la surcharge des opérateurs en C ++

En ce qui concerne la surcharge des opérateurs en C ++, vous devez suivre trois règles de base . Comme pour toutes ces règles, il existe en effet des exceptions. Parfois, les gens s'en sont écartés et le résultat n'était pas un mauvais code, mais ces écarts positifs sont rares. À tout le moins, 99 des 100 écarts de ce type que j'ai vus étaient injustifiés. Cependant, il pourrait tout aussi bien être de 999 sur 1000. Donc, vous feriez mieux de vous en tenir aux règles suivantes.

  1. Chaque fois que le sens d'un opérateur n'est pas clairement clair et incontesté, il ne doit pas être surchargé. Au lieu de cela, fournissez une fonction avec un nom bien choisi.
    Fondamentalement, la règle avant tout pour surcharger les opérateurs, en son cœur, dit: Ne le faites pas . Cela peut sembler étrange, car il y a beaucoup à savoir sur la surcharge des opérateurs et donc beaucoup d'articles, de chapitres de livres et d'autres textes traitent de tout cela. Mais malgré ces preuves apparemment évidentes, il n'y a que très peu de cas où la surcharge de l'opérateur est appropriée. La raison en est qu'en réalité il est difficile de comprendre la sémantique derrière l'application d'un opérateur à moins que l'utilisation de l'opérateur dans le domaine d'application soit bien connue et incontestée. Contrairement à la croyance populaire, ce n'est presque jamais le cas.

  2. Respectez toujours la sémantique bien connue de l'opérateur.
    C ++ ne pose aucune limitation sur la sémantique des opérateurs surchargés. Votre compilateur acceptera volontiers le code qui implémente l'+opérateurbinaireà soustraire de son opérande droit. Cependant, les utilisateurs d'un tel opérateur nedouterait jamais l'expressiona + bde soustraireadeb. Bien sûr, cela suppose que la sémantique de l'opérateur dans le domaine d'application est incontestée.

  3. Fournissez toujours tout sur un ensemble d'opérations connexes.
    Les opérateurs sont liés les uns aux autres et aux autres opérations. Si votre type le prend en chargea + b, les utilisateurs s'attendront également à pouvoir appelera += b. S'il prend en charge l'incrément de préfixe++a, ils s'attendronta++à fonctionner également. S'ils peuvent vérifier sia < b, ils s'attendent très certainement à pouvoir également vérifier sia > b. S'ils peuvent copier-construire votre type, ils s'attendent à ce que l'affectation fonctionne également.


Passez à La décision entre membre et non-membre .

sbi
la source
16
La seule chose que je sache qui viole l'un de ces derniers est boost::spiritlol.
Billy ONeal
66
@Billy: Selon certains, abuser +pour la concaténation de chaînes est une violation, mais c'est maintenant devenu une pratique bien établie, de sorte que cela semble naturel. Bien que je me souvienne d'une classe de cordes de brassage maison que j'ai vue dans les années 90 qui utilisait le binaire &à cette fin (se référant à BASIC pour la praxis établie). Mais, oui, le mettre dans la librairie std a fondamentalement mis cela dans la pierre. Il en va de même pour les abus <<et >>pour IO, BTW. Pourquoi le décalage à gauche serait-il l'opération de sortie évidente? Parce que nous l'avons tous appris quand nous avons vu notre premier "Bonjour, monde!" application. Et pour aucune autre raison.
sbi
5
@curiousguy: Si vous devez l'expliquer, ce n'est évidemment pas clair et incontesté. De même si vous avez besoin de discuter ou de défendre la surcharge.
sbi
5
@sbi: "l'évaluation par les pairs" est toujours une bonne idée. Pour moi, un opérateur mal choisi n'est pas différent d'un nom de fonction mal choisi (j'en ai vu beaucoup). L'opérateur n'est que des fonctions. Ni plus ni moins. Les règles sont les mêmes. Et pour comprendre si une idée est bonne, la meilleure façon est de comprendre combien de temps faut-il pour être comprise. (Par conséquent, l'examen par les pairs est un must, mais les pairs doivent être choisis entre des personnes exemptes de dogmes et de préjugés.)
Emilio Garavaglia
5
@sbi Pour moi, le seul fait absolument évident et incontestable operator==est qu'il devrait s'agir d'une relation d'équivalence (IOW, vous ne devriez pas utiliser de NaN non signalant). Il existe de nombreuses relations d'équivalence utiles sur les conteneurs. Que signifie l'égalité? " aégal b" signifie cela aet ba la même valeur mathématique. Le concept de valeur mathématique d'un (non-NaN) floatest clair, mais la valeur mathématique d'un conteneur peut avoir de nombreuses définitions utiles distinctes (de type récursif). La définition la plus forte de l'égalité est "ce sont les mêmes objets", et c'est inutile.
curiousguy
265

La syntaxe générale de la surcharge d'opérateur en C ++

Vous ne pouvez pas changer 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'utilisateur 1 . Autrement dit, au moins l'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 figurent: . :: sizeof typeid .*et le seul opérateur ternaire en C ++,?:

Les opérateurs qui peuvent être surchargés en C ++ sont les suivants:

  • opérateurs arithmétiques: + - * / %et += -= *= /= %=(tous les infixes binaires); + -(préfixe unaire); ++ --(préfixe unaire et suffixe)
  • manipulation de bits: & | ^ << >>et &= |= ^= <<= >>=(tous les infixes binaires); ~(préfixe unaire)
  • algèbre booléenne: == != < > <= >= || &&(tous les infixes binaires); !(préfixe unaire)
  • gestion de la mémoire: new new[] delete delete[]
  • opérateurs de conversion implicites
  • mélange: = [] -> ->* , (tous les infixes binaires); * &(tout préfixe unaire) ()(appel de fonction, infixe n-aire)

Cependant, le fait que vous pouvez surcharger tout cela ne signifie pas que vous devriez le faire. Voir les règles de base de la surcharge de l'opérateur.

En C ++, les opérateurs sont surchargés sous 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 en tant que fonction membre du type de leur opérande gauche, soit en tant que fonctions non membres . Que vous soyez libre de choisir ou obligé d'utiliser l'un ou l'autre dépend de plusieurs critères. 2 Un opérateur unaire @3 , appliqué à un objet x, est appelé soit comme, operator@(x)soit comme x.operator@(). Un opérateur d'infixe binaire @, appliqué aux objets xet y, est appelé soit comme, operator@(x,y)soit comme x.operator@(y). 4

Les opérateurs implémentés en tant que fonctions non membres sont parfois amis du type de leur opérande.

1 Le terme «défini par l'utilisateur» peut être légèrement trompeur. 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; à ces derniers 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 Ceci est couvert 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 espace réservé.

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é en tant que fonction membre.


Passez aux Trois règles de base de la surcharge d'opérateur en C ++ .

sbi
la source
~est un préfixe unaire, pas un infixe binaire.
mrkj
1
.*est absent de la liste des opérateurs non surchargeables.
celticminstrel
1
@Mateen Je voulais utiliser un espace réservé au lieu d'un véritable opérateur afin de préciser qu'il ne s'agit pas d'un opérateur spécial, mais qu'il s'applique à tous. Et, si vous voulez être un programmeur C ++, vous devez apprendre à faire attention même au petit format. :)
sbi
1
@HR: Si vous aviez lu ce guide, vous sauriez ce qui ne va pas. Je suggère généralement que vous lisiez les trois premières réponses liées à la question. Cela ne devrait pas durer plus d'une demi-heure de votre vie et vous donne une compréhension de base. La syntaxe spécifique à l'opérateur que vous pouvez rechercher ultérieurement. Votre problème spécifique vous suggère d'essayer de surcharger en operator+()tant que fonction membre, mais lui a donné la signature d'une fonction libre. Voyez ici .
sbi
1
@sbi: J'ai déjà lu les trois premiers articles et merci de les avoir faits. :) Je vais essayer de résoudre le problème sinon je pense qu'il vaut mieux le poser sur une question séparée. Merci encore d'avoir rendu la vie si facile pour nous! : D
Hosein Rahnama
251

La décision entre membre et non-membre

Les opérateurs binaires =(affectation), [](abonnement à un tableau), ->(accès membre), ainsi que l' ()opérateur n-aire (appel de fonction), doivent toujours être implémentés en tant que fonctions membres , car la syntaxe du langage le requiert.

D'autres opérateurs peuvent être implémentés en tant que membres ou non-membres. Cependant, certaines d'entre elles doivent généralement être implémentées en tant que fonctions non membres, car leur opérande gauche ne peut pas être modifié par vous. Les plus importants d'entre eux sont les opérateurs d'entrée et de sortie <<et >>, dont les opérandes de gauche sont des classes de flux de la bibliothèque standard que vous ne pouvez pas modifier.

Pour tous les opérateurs où vous devez choisir de les implémenter en tant que fonction membre ou fonction non membre, utilisez les règles générales suivantes pour décider:

  1. S'il s'agit d'un opérateur unaire , implémentez-le en tant que fonction membre .
  2. Si un opérateur binaire traite les deux opérandes de manière égale (il les laisse inchangés), implémentez cet opérateur en tant que fonction non membre .
  3. Si un opérateur binaire ne traite pas ses deux opérandes de la même manière (généralement il changera son opérande gauche), il pourrait être utile d'en faire une fonction membre du type de son opérande gauche, s'il doit accéder aux parties privées de l'opérande.

Bien sûr, comme pour toutes les règles de base, il existe des exceptions. Si vous avez un type

enum Month {Jan, Feb, ..., Nov, Dec}

et vous souhaitez surcharger les opérateurs d'incrémentation et de décrémentation pour cela, vous ne pouvez pas le faire en tant que fonctions membres, car en C ++, les types enum ne peuvent pas avoir de fonctions membres. Vous devez donc le surcharger comme une fonction gratuite. Et operator<()pour un modèle de classe imbriqué dans un modèle de classe, il est beaucoup plus facile d'écrire et de lire lorsqu'il est fait en tant que fonction membre en ligne dans la définition de classe. Mais ce sont en effet de rares exceptions.

(Cependant, si vous faites une exception, n'oubliez pas le problème de const-ness pour l'opérande qui, pour les fonctions membres, devient l' thisargument implicite . Si l'opérateur en tant que fonction non membre prend son argument le plus à gauche comme constréférence , le même opérateur qu'une fonction membre doit avoir un constà la fin pour faire *thisune constréférence.)


Passez à Opérateurs communs pour surcharger .

sbi
la source
9
L'article de Herb Sutter dans Effective C ++ (ou s'agit-il de normes de codage C ++?) Dit que l'on devrait préférer les fonctions non-membres non-amis aux fonctions membres, pour augmenter l'encapsulation de la classe. À mon humble avis, la raison de l'encapsulation a priorité sur votre règle de base, mais elle ne diminue pas la valeur de qualité de votre règle de base.
paercebal
8
@paercebal: Effective C ++ est de Meyers, C ++ Coding Standards de Sutter. À laquelle faites-vous référence? Quoi qu'il en soit, je n'aime pas l'idée, disons, de operator+=()ne pas être membre. Il doit changer son opérande gauche, donc par définition, il doit creuser profondément dans ses entrailles. Que gagneriez-vous à ne pas en faire membre?
sbi
9
@sbi: Élément 44 des normes de codage C ++ (Sutter) Préférez l'écriture de fonctions non membres non amis , bien sûr, cela ne s'applique que si vous pouvez réellement écrire cette fonction en utilisant uniquement l'interface publique de la classe. Si vous ne le pouvez pas (ou si cela peut nuire gravement aux performances), vous devez en faire membre ou ami.
Matthieu M.
3
@sbi: Oups, efficace, exceptionnel ... Pas étonnant que je mélange les noms. Quoi qu'il en soit, le gain est de limiter autant que possible le nombre de fonctions qui ont accès à un objet privé / protégé. De cette façon, vous augmentez l'encapsulation de votre classe, ce qui facilite sa maintenance / test / évolution.
paercebal
12
@sbi: Un exemple. Supposons que vous codiez une classe String, avec operator +=les appendméthodes et. La appendméthode est plus complète, car vous pouvez ajouter une sous-chaîne du paramètre de l'index i à l'index n -1: append(string, start, end)Il semble logique que l' +=appel ajoute avec start = 0et end = string.size. À ce moment, append pourrait être une méthode membre, mais operator +=n'a pas besoin d'être membre, et en faire un non-membre diminuerait la quantité de code jouant avec les entrailles String, donc c'est une bonne chose .... ^ _ ^ ...
paercebal
165

Opérateurs de conversion (également appelés conversions définies par l'utilisateur)

En C ++, vous pouvez créer des opérateurs de conversion, des opérateurs qui permettent au compilateur de convertir entre vos types et d'autres types définis. Il existe deux types d'opérateurs de conversion, implicites et explicites.

Opérateurs de conversion implicites (C ++ 98 / C ++ 03 et C ++ 11)

Un opérateur de conversion implicite permet au compilateur de convertir implicitement (comme la conversion entre intet long) la valeur d'un type défini par l'utilisateur en un autre type.

Voici une classe simple avec un opérateur de conversion implicite:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Les opérateurs de conversion implicites, comme les constructeurs à un argument, sont des conversions définies par l'utilisateur. Les compilateurs accordent une conversion définie par l'utilisateur lorsqu'ils tentent de faire correspondre un appel à une fonction surchargée.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

Au début, cela semble très utile, mais le problème avec cela est que la conversion implicite entre même en action lorsqu'elle n'est pas attendue. Dans le code suivant, void f(const char*)sera appelé car my_string()n'est pas une valeur l , donc la première ne correspond pas:

void f(my_string&);
void f(const char*);

f(my_string());

Les débutants se trompent facilement et même les programmeurs C ++ expérimentés sont parfois surpris parce que le compilateur choisit une surcharge qu'ils ne soupçonnaient pas. Ces problèmes peuvent être atténués par des opérateurs de conversion explicites.

Opérateurs de conversion explicite (C ++ 11)

Contrairement aux opérateurs de conversion implicites, les opérateurs de conversion explicites n'interviendront jamais lorsque vous ne vous y attendez pas. Voici une classe simple avec un opérateur de conversion explicite:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Remarquez le explicit. Maintenant, lorsque vous essayez d'exécuter le code inattendu à partir des opérateurs de conversion implicites, vous obtenez une erreur de compilation:

prog.cpp: Dans la fonction 'int main ()':
prog.cpp: 15: 18: erreur: pas de fonction correspondante pour l'appel à 'f (my_string)'
prog.cpp: 15: 18: note: les candidats sont:
prog.cpp: 11: 10: note: void f (my_string &)
prog.cpp: 11: 10: note: aucune conversion connue pour l'argument 1 de 'my_string' en 'my_string &'
prog.cpp: 12: 10: note: void f (const char *)
prog.cpp: 12: 10: note: pas de conversion connue pour l'argument 1 de 'my_string' en 'const char *'

Pour appeler l'opérateur de transtypage explicite, vous devez utiliser static_castun transtypage de style C ou un transtypage de style constructeur (c'est-à-dire T(value)).

Cependant, il existe une exception: le compilateur est autorisé à effectuer une conversion implicite vers bool. De plus, le compilateur n'est pas autorisé à effectuer une autre conversion implicite après sa conversion en bool(un compilateur est autorisé à effectuer 2 conversions implicites à la fois, mais seulement 1 conversion définie par l'utilisateur au maximum).

Étant donné que le compilateur ne transtypera pas "passé" bool, les opérateurs de conversion explicites suppriment désormais le besoin de l' idiome Safe Bool . Par exemple, les pointeurs intelligents avant C ++ 11 utilisaient l'idiome Safe Bool pour empêcher les conversions en types intégraux. En C ++ 11, les pointeurs intelligents utilisent à la place un opérateur explicite car le compilateur n'est pas autorisé à convertir implicitement en un type intégral après avoir converti explicitement un type en bool.

Continuez vers Surcharge newetdelete .

JKor
la source
148

Surcharge newetdelete

Remarque: Cela ne concerne que la syntaxe de la surchargenewetdeletenon la mise en œuvre de tels opérateurs surchargés. Je pense que la sémantique de la surchargenew et deletemérite leur propre FAQ , dans le sujet de la surcharge de l'opérateur, je ne peux jamais lui rendre justice.

Les bases

En C ++, lorsque vous écrivez une nouvelle expression comme new T(arg)deux choses se produisent lorsque cette expression est évaluée: Tout d' abord operator newest invoquée pour obtenir la mémoire brute, puis le constructeur approprié de Test invoqué pour transformer cette mémoire brute en un objet valide. De même, lorsque vous supprimez un objet, son destructeur est d'abord appelé, puis la mémoire est renvoyée operator delete.
C ++ vous permet de régler ces deux opérations: la gestion de la mémoire et la construction / destruction de l'objet sur la mémoire allouée. Ce dernier se fait en écrivant des constructeurs et des destructeurs pour une classe. La gestion précise de la mémoire se fait en écrivant les vôtres operator newet operator delete.

La première des règles de base de la surcharge de l'opérateur - ne le faites pas - s'applique particulièrement à la surcharge newet à delete. Presque les seules raisons de surcharger ces opérateurs sont les problèmes de performances et les contraintes de mémoire , et dans de nombreux cas, d'autres actions, telles que les modifications des algorithmes utilisés, fourniront un rapport coût / gain beaucoup plus élevé que d'essayer de modifier la gestion de la mémoire.

La bibliothèque standard C de est livré avec un ensemble de prédéfinis newet les deleteopérateurs. Les plus importants sont les suivants:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

Les deux premiers allouent / désallouent de la mémoire pour un objet, les deux derniers pour un tableau d'objets. Si vous fournissez vos propres versions de celles-ci, elles ne surchargeront pas, mais remplaceront celles de la bibliothèque standard.
Si vous surchargez operator new, vous devez toujours surcharger également la correspondance operator delete, même si vous n'avez jamais l'intention de l'appeler. La raison en est que, si un constructeur lance lors de l'évaluation d'une nouvelle expression, le système d'exécution retournera la mémoire à la operator deletecorrespondance operator newqui a été appelée pour allouer la mémoire pour créer l'objet. Si vous ne fournissez pas de correspondance operator delete, celui par défaut est appelé, ce qui est presque toujours faux.
Si vous surchargez newet delete, vous devez également envisager de surcharger les variantes de tableau.

Placement new

C ++ permet aux nouveaux opérateurs et aux opérateurs de suppression de prendre des arguments supplémentaires.
Le nouveau placement dit vous permet de créer un objet à une certaine adresse qui est transmise à:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

La bibliothèque standard est livrée avec les surcharges appropriées des opérateurs new et delete pour cela:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Notez que, dans l'exemple de code pour le placement nouveau donné ci-dessus, operator deleten'est jamais appelé, sauf si le constructeur de X lève une exception.

Vous pouvez également surcharger newet deleteavec d'autres arguments. Comme pour l'argument supplémentaire pour placement new, ces arguments sont également répertoriés entre parenthèses après le mot clé new. Simplement pour des raisons historiques, de telles variantes sont souvent également appelées placement nouveau, même si leurs arguments ne sont pas pour placer un objet à une adresse spécifique.

Nouveau et suppression spécifiques à la classe

Le plus souvent, vous souhaiterez affiner la gestion de la mémoire car la mesure a montré que les instances d'une classe spécifique ou d'un groupe de classes associées sont souvent créées et détruites et que la gestion de la mémoire par défaut du système d'exécution, réglée pour performances générales, traite inefficacement dans ce cas spécifique. Pour améliorer cela, vous pouvez surcharger new et delete pour une classe spécifique:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

Surchargé ainsi, new et delete se comportent comme des fonctions membres statiques. Pour les objets de my_class, l' std::size_targument sera toujours sizeof(my_class). Cependant, ces opérateurs sont également appelés pour les objets alloués dynamiquement des classes dérivées , auquel cas il pourrait être supérieur à cela.

Nouveau global et supprimer

Pour surcharger le nouveau global et le supprimer, remplacez simplement les opérateurs prédéfinis de la bibliothèque standard par les nôtres. Cependant, cela doit rarement être fait.

sbi
la source
11
Je ne suis pas d'accord non plus pour dire que le remplacement de l'opérateur global new et delete est généralement pour des performances: au contraire, c'est généralement pour le suivi des bogues.
Yttrill
1
Vous devez également noter que si vous utilisez un nouvel opérateur surchargé, vous devez également fournir un opérateur de suppression avec des arguments correspondants. Vous dites que dans la section sur les nouveautés / suppressions globales où cela n'a pas beaucoup d'intérêt.
Yttrill
13
@Yttrill, vous confondez les choses. Le sens est surchargé. Ce que signifie "surcharge de l'opérateur", c'est que la signification est surchargée. Cela ne signifie pas que les fonctions sont littéralement surchargées, et en particulier, l' opérateur new ne surchargera pas la version Standard. @sbi ne prétend pas le contraire. Il est courant de l'appeler "surcharge nouvelle" autant qu'il est courant de dire "opérateur d'addition surchargé".
Johannes Schaub - litb
1
@sbi: Voir (ou mieux, lien vers) gotw.ca/publications/mill15.htm . Ce n'est qu'une bonne pratique envers les personnes qui utilisent parfois de nothrownouvelles.
Alexandre C.
1
"Si vous ne fournissez pas de suppression d'opérateur correspondante, celle par défaut est appelée" -> En fait, si vous ajoutez des arguments et ne créez pas de suppression correspondante, aucune suppression d'opérateur n'est appelée du tout et vous avez une fuite de mémoire. (15.2.2, le stockage occupé par l'objet n'est désalloué que si une suppression d'opérateur appropriée est trouvée)
dascandy
46

Pourquoi la operator<<fonction de streaming d'objets vers std::coutou vers un fichier ne peut-elle pas être une fonction membre?

Disons que vous avez:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Cela dit, vous ne pouvez pas utiliser:

Foo f = {10, 20.0};
std::cout << f;

Puisque operator<<est surchargé en tant que fonction membre de Foo, le LHS de l'opérateur doit être un Fooobjet. Cela signifie que vous devrez utiliser:

Foo f = {10, 20.0};
f << std::cout

ce qui est très peu intuitif.

Si vous le définissez comme une fonction non membre,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

Vous pourrez utiliser:

Foo f = {10, 20.0};
std::cout << f;

ce qui est très intuitif.

R Sahu
la source