Penser en C++

Volume 1


précédentsommairesuivant

12. Surcharges d'opérateurs

La surcharge d'opérateurs n'est rien d'autre qu'une “douceur syntaxique,” ce qui signifie qu'il s'agit simplement d'une autre manière pour vous de faire un appel de fonction .

La différence est que les arguments de cette fonction n'apparaissent pas entre parenthèses , mais qu'au contraire ils entourent ou sont près de symboles dont vous avez toujours pensé qu'ils étaient immuables.

Il y a deux différences entre l'utilisation d'un opérateur et un appel de fonction ordinaire. La syntaxe est différente; un opérateur est souvent “appelé” en le plaçant entre ou parfois après les arguments. La seconde différence est que le compilateur détermine quelle “fonction” appeler. Par exemple, si vous utilisez l'opérateur + avec des arguments flottants, le compilateur “appelle” la fonction pour effectuer l'addition des flottants (cet “appel” consiste principalement en l'insertion de code en-ligne (substitution de code à l'appel) , ou bien une instruction machine du prépocesseur arithmétique). Si vous utilisez l'opérateur + avec un flottant et un entier,le compilateur “appelle” une fonction spéciale pour convertir l' int en un float, puis après “appelle” le code d'addition des flottants.

Mais en C++, il est possible de définir de nouveaux opérateurs qui fonctionnent avec les classes. Cette définition n'est rien d'autre qu'une définition de fonction ordinaire sauf que le nom de la fonction est constitué du mot-clé operator suivi par l'opérateur. C'est la seule différence, et cela devient une fonction comme toute autre fonction, que le compilateur appelle lorsqu'il voit le modèle correspondant.

12.1. Soyez avertis et rassurés

Il est tentant de s'enthousiasmer plus que nécessaire avec la surcharge des opérateurs. Au début c'est un joujou. Mais gardez à l'esprit que ce n'est qu'une douceur syntaxique, une autre manière d'appeler une fonction. En voyant les choses comme ça, vous n'avez aucune raison de surcharger un opérateur sauf s'il rend le code concernant votre classe plus facile à écrire et surtout plus facile à lire. (Souvenez vous que le code est beaucoup plus souvent lu qu'il n'est écrit) Si ce n'est pas le cas, ne vous ennuyez pas avec ça.

La panique est une autre réponse habituelle à la surcharge des opérateurs; soudain, les opérateurs C n'ont plus leur sens usuel. "Tout est changé et tout mon code C va faire des choses différentes !" Ce n'est pas vrai. Tous les opérateurs utilisés dans des expressions ne contenant que des types prédéfinis ne peuvent pas être changés. Vous ne pouvez jamais surcharger des opérateurs de telle manière que:

 
Sélectionnez
1 << 4;

se comporte différemment, ou que

 
Sélectionnez
1.414 << 2;

ait un sens. Seule une expression contenant un type défini par l'utilisateur peut contenir un opérateur surchargé.

12.2. Syntaxe

Définir un opérateur surchargé c'est comme définir une fonction, mais le nom de cette fonction est operator@, où @ représente l'opérateur qui est surchargé. Le nombre d'arguments dans la liste des arguments de l'opérateur dépend de deux facteurs:

  1. Si c'est un opérateur unaire (un argument) ou un opérateur binaire (deux arguments).
  2. Si l'opérateur est défini comme une fonction globale (un argument si unaire, deux si binaire) ou bien une fonction membre (zero argument si unaire, un si binaire - l'objet devenant alors l'argument de gauche).

Voici une petite classe qui montre la surcharge d'opérateurs:

 
Sélectionnez
//: C12:OperatorOverloadingSyntax.cpp
#include <iostream>
using namespace std;
 
class Integer {
  int i;
public:
  Integer(int ii) : i(ii) {}
  const Integer
  operator+(const Integer& rv) const {
    cout << "operator+" << endl;
    return Integer(i + rv.i);
  }
  Integer&
  operator+=(const Integer& rv) {
    cout << "operator+=" << endl;
    i += rv.i;
    return *this;
  }
};
 
int main() {
  cout << "built-in types:" << endl;
  int i = 1, j = 2, k = 3;
  k += i + j;
  cout << "user-defined types:" << endl;
  Integer ii(1), jj(2), kk(3);
  kk += ii + jj;
} ///:~

Les deux opérateurs surchargés sont définis comme des fonctions membres 'inline' qui avertissent quand elles sont appelées. L'argument unique est ce qui apparaît à droite de l'opérateur pour les opérateurs binaires. Les opérateurs unaires n'ont pas d'arguments lorsqu'ils sont définis comme des fonctons membres. La fonction membre est appelée pour l'objet qui se trouve à gauche de l'opérateur.

Pour les opérateurs non conditionnels (les conditionnels retournent d'habitude un booléen), vous souhaiterez presque toujours retourner un objet ou une référence du même type que ceux avec lesquels vous travaillez si les deux arguments sont du même type. (S'ils ne sont pas du même type, l'interprétation du résultat est à votre charge.) De la sorte, des expressons complexes peuvent être formées:

 
Sélectionnez
kk += ii + jj;

L' operator+ produit un nouvel Entier(temporaire) qui est utilisé comme l'argument rv pour l' operator+=. Cette valeur temporaire est détruite aussitôt qu'elle n'est plus nécessaire.

12.3. Opérateurs surchargeables

Bien que vous puissiez surcharger tous les opérateurs disponibles en C, l'utilisation de la surcharge d'opérateurs comporte des restrictions notoires. En particulier, vous ne pouvez faire des combinaisons d'opérateurs qui, en l'état actuel des choses n'ont aucun sens en C (comme ** pour représenter l'exponentiation), vous ne pouvez changer la priorité des opérateurs, et vous ne pouvez changer l'arité (nombre d'arguments requis) pour un opérateur. Tout cela a une raison - toutes ces actions engendreraient des opérateurs plus susceptibles d'apporter de la confusion que de la clarté.

Les deux sous-sections suivantes donnent des exemples de tous les opérateurs “réguliers”, surchargés sous la forme que vous êtes le plus susceptible d'utiliser.

12.3.1. Opérateurs unaires

L'exemple suivant montre la syntaxe pour surcharger tous les opérateurs unaires, à la fois sous la forme de fonctions globales (non-membres amies) et comme fonctions membres. Il étendra la classe Integer montrée auparavant et ajoutera une nouvelle classe byte. La signification de vos opérateurs particuliers dépendra de la façon dont vous voulez les utiliser, mais prenez en considération le programmeur client avant de faire quelque chose d'inattendu.

Voici un catalogue de toutes les fonctions unaires:

 
Sélectionnez
//: C12:OverloadingUnaryOperators.cpp
#include <iostream>
using namespace std;
 
// fonctions non membres:
class Integer {
  long i;
  Integer* This() { return this; }
public:
  Integer(long ll = 0) : i(ll) {}
  // pas d'effet de bord prend un argument const&
  friend const Integer&
    operator+(const Integer& a);
  friend const Integer
    operator-(const Integer& a);
  friend const Integer
    operator~(const Integer& a);
  friend Integer*
    operator&(Integer& a);
  friend int
    operator!(const Integer& a);
  // Pour les effets de bord argument non-const& :
  // Préfixé :
  friend const Integer&
    operator++(Integer& a);
  // Postfixé :
  friend const Integer
    operator++(Integer& a, int);
  // Préfixé :
  friend const Integer&
    operator--(Integer& a);
  // Postfixé :
  friend const Integer
    operator--(Integer& a, int);
};
 
// Opérateurs globaux:
const Integer& operator+(const Integer& a) {
  cout << "+Integer\n";
  return a; // le + unaire n'a aucun effet
}
const Integer operator-(const Integer& a) {
  cout << "-Integer\n";
  return Integer(-a.i);
}
const Integer operator~(const Integer& a) {
  cout << "~Integer\n";
  return Integer(~a.i);
}
Integer* operator&(Integer& a) {
  cout << "&Integer\n";
  return a.This(); // &a est récursif!
}
int operator!(const Integer& a) {
  cout << "!Integer\n";
  return !a.i;
}
// Préfixé ; retourne une valeur incrémentée :
const Integer& operator++(Integer& a) {
  cout << "++Integer\n";
  a.i++;
  return a;
}
// Postfixé ; retourne la valeur avant l'incrémentation :
const Integer operator++(Integer& a, int) {
  cout << "Integer++\n";
  Integer before(a.i);
  a.i++;
  return before;
}
// Préfixé ; return la valeur décrémentée :
const Integer& operator--(Integer& a) {
  cout << "--Integer\n";
  a.i--;
  return a;
}
// Postfixé ; retourne la valeur avant décrémentation :
const Integer operator--(Integer& a, int) {
  cout << "Integer--\n";
  Integer before(a.i);
  a.i--;
  return before;
}
 
// Montre que les opérateurs surchargés fonctionnent:
void f(Integer a) {
  +a;
  -a;
  ~a;
  Integer* ip = &a;
  !a;
  ++a;
  a++;
  --a;
  a--;
}
 
// Fonctions membres ( "this" implicite):
class Byte {
  unsigned char b;
public:
  Byte(unsigned char bb = 0) : b(bb) {}
  // Pas d'effet de bord : fonction membre const :
  const Byte& operator+() const {
    cout << "+Byte\n";
    return *this;
  }
  const Byte operator-() const {
    cout << "-Byte\n";
    return Byte(-b);
  }
  const Byte operator~() const {
    cout << "~Byte\n";
    return Byte(~b);
  }
  Byte operator!() const {
    cout << "!Byte\n";
    return Byte(!b);
  }
  Byte* operator&() {
    cout << "&Byte\n";
    return this;
  }
  // Effets de bord : fonction membre non-const :
  const Byte& operator++() { // Préfixé
    cout << "++Byte\n";
    b++;
    return *this;
  }
  const Byte operator++(int) { // Postfixé
    cout << "Byte++\n";
    Byte before(b);
    b++;
    return before;
  }
  const Byte& operator--() { // Préfixé
    cout << "--Byte\n";
    --b;
    return *this;
  }
  const Byte operator--(int) { // Postfixé
    cout << "Byte--\n";
    Byte before(b);
    --b;
    return before;
  }
};
 
void g(Byte b) {
  +b;
  -b;
  ~b;
  Byte* bp = &b;
  !b;
  ++b;
  b++;
  --b;
  b--;
}
 
int main() {
  Integer a;
  f(a);
  Byte b;
  g(b);
} ///:~

Les fonctions sont regroupées suivant la façon dont leurs arguments sont passés. Des conseils pour passer et retourner des arguments seront donnés plus loin. Les formes ci-dessus (et celles qui suivent dans la section suivante) correspondent typiquement à ce que vous utiliserez, aussi commencez en les prenant comme modèles quand vous commencerez à surcharger vos propres opérateurs.

Incrémentation et décrémentation

Les surcharges de ++ et -- font l'objet d'un dilemne parce que vous voulez pouvoir appeler différentes fonctions selon qu'elles apparaissent avant (préfixé) ou après (postfixé) l'objet sur lequel elles agissent. La solution est simple, mais les gens trouvent parfois cela un peu embrouillé au premier abord. Lorsque le compilateur voit, par exemple, ++a(une pré-incrémentation), il provoque un appel à operator++(a); mais lorsque il voit a++, il provoque un appel à operator++(a, int). Ce qui signifie que, le compilateur différencie les deux formes en faisant des appels à différentes fonctions surchargées. Dans OverloadingUnaryOperators.cpp pour les versions des fonctions membres, si le compilateur voit ++b, il provoque un appel à B::operator++( ); s'il voit b++ il appelle B::operator++(int).

Tout ce que l'utilisateur voit c'est qu'une fonction différente est appelée pour les versions préfixée et postfixée. Dans le fond des choses, toutefois, les deux appels de fonctions ont des signatures différentes, ainsi ils font des liaisons avec des corps de fonctions différents. Le compilateur passe une valeur constante factice pour l'argument int(que l'on ne nomme jamais puisque la valeur n'est jamais utilisée) pour générer la signature spécifique pour la version postfixée.

12.3.2. Opérateurs binaires

Le listing qui suit reproduit l'exemples de OverloadingUnaryOperators.cpp pour les opérateurs binaires de sorte que vous ayez un exemple de tous les opérateurs que vous pourriez vouloir surcharger. Cette fois encore, nous donnons les versions fonctions globales et fonctions-membres.

 
Sélectionnez
//: C12:Integer.h
// surcharge non-membres
#ifndef INTEGER_H
#define INTEGER_H
#include <iostream>
 
// fonctions non-membres:
class Integer { 
  long i;
public:
  Integer(long ll = 0) : i(ll) {}
  // opérateurs créant une valeur nouvelle modifiée :
  friend const Integer
    operator+(const Integer& left,
              const Integer& right);
  friend const Integer
    operator-(const Integer& left,
              const Integer& right);
  friend const Integer
    operator*(const Integer& left,
              const Integer& right);
  friend const Integer
    operator/(const Integer& left,
              const Integer& right);
  friend const Integer
    operator%(const Integer& left,
              const Integer& right);
  friend const Integer
    operator^(const Integer& left,
              const Integer& right);
  friend const Integer
    operator&(const Integer& left,
              const Integer& right);
  friend const Integer
    operator|(const Integer& left,
              const Integer& right);
  friend const Integer
    operator<<(const Integer& left,
               const Integer& right);
  friend const Integer
    operator>>(const Integer& left,
               const Integer& right);
  // Affectations combinées & retourne une lvalue :
  friend Integer&
    operator+=(Integer& left,
               const Integer& right);
  friend Integer&
    operator-=(Integer& left,
               const Integer& right);
  friend Integer&
    operator*=(Integer& left,
               const Integer& right);
  friend Integer&
    operator/=(Integer& left,
               const Integer& right);
  friend Integer&
    operator%=(Integer& left,
               const Integer& right);
  friend Integer&
    operator^=(Integer& left,
               const Integer& right);
  friend Integer&
    operator&=(Integer& left,
               const Integer& right);
  friend Integer&
    operator|=(Integer& left,
               const Integer& right);
  friend Integer&
    operator>>=(Integer& left,
                const Integer& right);
  friend Integer&
    operator<<=(Integer& left,
                const Integer& right);
  // Opérateurs conditionnels retournent true/false :
  friend int
    operator==(const Integer& left,
               const Integer& right);
  friend int
    operator!=(const Integer& left,
               const Integer& right);
  friend int
    operator<(const Integer& left,
              const Integer& right);
  friend int
    operator>(const Integer& left,
              const Integer& right);
  friend int
    operator<=(const Integer& left,
               const Integer& right);
  friend int
    operator>=(const Integer& left,
               const Integer& right);
  friend int
    operator&&(const Integer& left,
               const Integer& right);
  friend int
    operator||(const Integer& left,
               const Integer& right);
  // Ecrit le contenu dans un ostream :
  void print(std::ostream& os) const { os << i; }
}; 
#endif // INTEGER_H ///:~
 
Sélectionnez
//: C12:Integer.cpp {O}
// Implementation des opérateurs surchargés
#include "Integer.h"
#include "../require.h"
 
const Integer
  operator+(const Integer& left,
            const Integer& right) {
  return Integer(left.i + right.i);
}
const Integer
  operator-(const Integer& left,
            const Integer& right) {
  return Integer(left.i - right.i);
}
const Integer
  operator*(const Integer& left,
            const Integer& right) {
  return Integer(left.i * right.i);
}
const Integer
  operator/(const Integer& left,
            const Integer& right) {
  require(right.i != 0, "division par zéro");
  return Integer(left.i / right.i);
}
const Integer
  operator%(const Integer& left,
            const Integer& right) {
  require(right.i != 0, "modulo zéro");
  return Integer(left.i % right.i);
}
const Integer
  operator^(const Integer& left,
            const Integer& right) {
  return Integer(left.i ^ right.i);
}
const Integer
  operator&(const Integer& left,
            const Integer& right) {
  return Integer(left.i & right.i);
}
const Integer
  operator|(const Integer& left,
            const Integer& right) {
  return Integer(left.i | right.i);
}
const Integer
  operator<<(const Integer& left,
             const Integer& right) {
  return Integer(left.i << right.i);
}
const Integer
  operator>>(const Integer& left,
             const Integer& right) {
  return Integer(left.i >> right.i);
}
// Affectations modifient & renvoient une lvalue :
Integer& operator+=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-affectation */}
   left.i += right.i;
   return left;
}
Integer& operator-=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-affectation */}
   left.i -= right.i;
   return left;
}
Integer& operator*=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-affectation */}
   left.i *= right.i;
   return left;
}
Integer& operator/=(Integer& left,
                    const Integer& right) {
   require(right.i != 0, "division par zéro");
   if(&left == &right) {/*auto-affectation */}
   left.i /= right.i;
   return left;
}
Integer& operator%=(Integer& left,
                    const Integer& right) {
   require(right.i != 0, "modulo zéro");
   if(&left == &right) {/* auto-affectation */}
   left.i %= right.i;
   return left;
}
Integer& operator^=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-affectation */}
   left.i ^= right.i;
   return left;
}
Integer& operator&=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-affectation */}
   left.i &= right.i;
   return left;
}
Integer& operator|=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-affectation */}
   left.i |= right.i;
   return left;
}
Integer& operator>>=(Integer& left,
                     const Integer& right) {
   if(&left == &right) {/* auto-affectation */}
   left.i >>= right.i;
   return left;
}
Integer& operator<<=(Integer& left,
                     const Integer& right) {
   if(&left == &right) {/* auto-affectation */}
   left.i <<= right.i;
   return left;
}
// les opérateurs conditionnels renvoient true/false :
int operator==(const Integer& left,
               const Integer& right) {
    return left.i == right.i;
}
int operator!=(const Integer& left,
               const Integer& right) {
    return left.i != right.i;
}
int operator<(const Integer& left,
              const Integer& right) {
    return left.i < right.i;
}
int operator>(const Integer& left,
              const Integer& right) {
    return left.i > right.i;
}
int operator<=(const Integer& left,
               const Integer& right) {
    return left.i <= right.i;
}
int operator>=(const Integer& left,
               const Integer& right) {
    return left.i >= right.i;
}
int operator&&(const Integer& left,
               const Integer& right) {
    return left.i && right.i;
}
int operator||(const Integer& left,
               const Integer& right) {
    return left.i || right.i;
} ///:~
 
Sélectionnez
//: C12:IntegerTest.cpp
//{L} Integer
#include "Integer.h"
#include <fstream>
using namespace std;
ofstream out("IntegerTest.out");
 
void h(Integer& c1, Integer& c2) {
  // Une expression complexe :
  c1 += c1 * c2 + c2 % c1;
  #define TRY(OP) \
    out << "c1 = "; c1.print(out); \
    out << ", c2 = "; c2.print(out); \
    out << ";  c1 " #OP " c2 produit "; \
    (c1 OP c2).print(out); \
    out << endl;
  TRY(+) TRY(-) TRY(*) TRY(/)
  TRY(%) TRY(^) TRY(&) TRY(|)
  TRY(<<) TRY(>>) TRY(+=) TRY(-=)
  TRY(*=) TRY(/=) TRY(%=) TRY(^=)
  TRY(&=) TRY(|=) TRY(>>=) TRY(<<=)
  // Conditionelles:
  #define TRYC(OP) \
    out << "c1 = "; c1.print(out); \
    out << ", c2 = "; c2.print(out); \
    out << ";  c1 " #OP " c2 produit "; \
    out << (c1 OP c2); \
    out << endl;
  TRYC(<) TRYC(>) TRYC(==) TRYC(!=) TRYC(<=)
  TRYC(>=) TRYC(&&) TRYC(||)
} 
 
int main() {
  cout << "fonctions amies" << endl;
  Integer c1(47), c2(9);
  h(c1, c2);
} ///:~
 
Sélectionnez
//: C12:Byte.h
// opérateurs surchargés par des membres
#ifndef BYTE_H
#define BYTE_H
#include "../require.h"
#include <iostream>
// fonctions membres ("this" implicite) :
class Byte { 
  unsigned char b;
public:
  Byte(unsigned char bb = 0) : b(bb) {}
  // Pas d'effet de bord: fonction membre const:
  const Byte
    operator+(const Byte& right) const {
    return Byte(b + right.b);
  }
  const Byte
    operator-(const Byte& right) const {
    return Byte(b - right.b);
  }
  const Byte
    operator*(const Byte& right) const {
    return Byte(b * right.b);
  }
  const Byte
    operator/(const Byte& right) const {
    require(right.b != 0, "division par zéro");
    return Byte(b / right.b);
  }
  const Byte
    operator%(const Byte& right) const {
    require(right.b != 0, "modulo zéro");
    return Byte(b % right.b);
  }
  const Byte
    operator^(const Byte& right) const {
    return Byte(b ^ right.b);
  }
  const Byte
    operator&(const Byte& right) const {
    return Byte(b & right.b);
  }
  const Byte
    operator|(const Byte& right) const {
    return Byte(b | right.b);
  }
  const Byte
    operator<<(const Byte& right) const {
    return Byte(b << right.b);
  }
  const Byte
    operator>>(const Byte& right) const {
    return Byte(b >> right.b);
  }
  // Les affectations modifient & renvoient une lvalue.
  // operator= ne peut être qu'une fonction membre :
  Byte& operator=(const Byte& right) {
    // traite l' auto-affectation:
    if(this == &right) return *this;
    b = right.b;
    return *this;
  }
  Byte& operator+=(const Byte& right) {
    if(this == &right) {/* auto-affectation */}
    b += right.b;
    return *this;
  }
  Byte& operator-=(const Byte& right) {
    if(this == &right) {/* auto-affectation */}
    b -= right.b;
    return *this;
  }
  Byte& operator*=(const Byte& right) {
    if(this == &right) {/* auto-affectation */}
    b *= right.b;
    return *this;
  }
  Byte& operator/=(const Byte& right) {
    require(right.b != 0, "division par zéro");
    if(this == &right) {/* auto-affectation */}
    b /= right.b;
    return *this;
  }
  Byte& operator%=(const Byte& right) {
    require(right.b != 0, "modulo zéro");
    if(this == &right) {/* auto-affectation */}
    b %= right.b;
    return *this;
  }
  Byte& operator^=(const Byte& right) {
    if(this == &right) {/* auto-affectation */}
    b ^= right.b;
    return *this;
  }
  Byte& operator&=(const Byte& right) {
    if(this == &right) {/* auto-affectation */}
    b &= right.b;
    return *this;
  }
  Byte& operator|=(const Byte& right) {
    if(this == &right) {/* auto-affectation */}
    b |= right.b;
    return *this;
  }
  Byte& operator>>=(const Byte& right) {
    if(this == &right) {/* auto-affectation */}
    b >>= right.b;
    return *this;
  }
  Byte& operator<<=(const Byte& right) {
    if(this == &right) {/* auto-affectation */}
    b <<= right.b;
    return *this;
  }
  // les opérateurs conditionnels renvoient true/false :
  int operator==(const Byte& right) const {
      return b == right.b;
  }
  int operator!=(const Byte& right) const {
      return b != right.b;
  }
  int operator<(const Byte& right) const {
      return b < right.b;
  }
  int operator>(const Byte& right) const {
      return b > right.b;
  }
  int operator<=(const Byte& right) const {
      return b <= right.b;
  }
  int operator>=(const Byte& right) const {
      return b >= right.b;
  }
  int operator&&(const Byte& right) const {
      return b && right.b;
  }
  int operator||(const Byte& right) const {
      return b || right.b;
  }
  // Ecrit le contenu dans un ostream:
  void print(std::ostream& os) const {
    os << "0x" << std::hex << int(b) << std::dec;
  }
}; 
#endif // BYTE_H ///:~
 
Sélectionnez
//: C12:ByteTest.cpp
#include "Byte.h"
#include <fstream>
using namespace std;
ofstream out("ByteTest.out");
 
void k(Byte& b1, Byte& b2) {
  b1 = b1 * b2 + b2 % b1;
 
  #define TRY2(OP) \
    out << "b1 = "; b1.print(out); \
    out << ", b2 = "; b2.print(out); \
    out << ";  b1 " #OP " b2 produit "; \
    (b1 OP b2).print(out); \
    out << endl;
 
  b1 = 9; b2 = 47;
  TRY2(+) TRY2(-) TRY2(*) TRY2(/)
  TRY2(%) TRY2(^) TRY2(&) TRY2(|)
  TRY2(<<) TRY2(>>) TRY2(+=) TRY2(-=)
  TRY2(*=) TRY2(/=) TRY2(%=) TRY2(^=)
  TRY2(&=) TRY2(|=) TRY2(>>=) TRY2(<<=)
  TRY2(=) // opérateur d'affectation
 
  // Conditionelles:
  #define TRYC2(OP) \
    out << "b1 = "; b1.print(out); \
    out << ", b2 = "; b2.print(out); \
    out << ";  b1 " #OP " b2 produit "; \
    out << (b1 OP b2); \
    out << endl;
 
  b1 = 9; b2 = 47;
  TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
  TRYC2(>=) TRYC2(&&) TRYC2(||)
 
  // affectations en série:
  Byte b3 = 92;
  b1 = b2 = b3;
}
 
int main() {
  out << "fonctions membres :" << endl;
  Byte b1(47), b2(9);
  k(b1, b2);
} ///:~

Vous pouvez voir que operator= ne peut être qu'une fonction membre. Cela est expliqué plus loin.

Notez que tous les opérateurs d'affectation ont une portion de code pour vérifier l'auto-affectation ; c'est une règle générale. Dans certains cas ce n'est pas nécessaire ; par exemple, avec operator+= vous voulez souvent dire A+=A et vouloir que A s'ajoute à lui-même. L'endroit le plus important à vérifier pour l'auto-affectation est operator= parce qu'avec des objets complexes les conséquences peuvent être désastreuses. (Dans certains cas ça passe, mais vous devez toujours garder cela à l'esprit en écrivant operator=.)

Tous les opérateurs montrés dans les deux exemples précédents sont surchargés pour manipuler un type unique. Il est également possible de surcharger des opérateurs pour manipuler en même temps des types distincts, pour vous permettre d'additionner des pommes et des oranges, par exemple. Avant de vous lancer dans une surcharge exhaustive des opérateurs, toutefois, vous devriez jeter un coup d'oeil à la section consacrée aux conversions automatiques de types plus loin dans ce chapitre. Souvent, une conversion de type au bon endroit peut vous faire faire l'économie d'un grand nombre de surcharges d'opérateurs.

12.3.3. Arguments & valeurs de retour

Cela peut sembler un peu troublant tout d'abord quand vous regardez OverloadingUnaryOperators.cpp, Integer.h et Byte.h et que vous voyez toutes les différentes façons dont les arguments sont passés et renvoyés. Bien que vous puissiez passer et retourner des arguments comme vous l'entendez, les choix dans ces exemples n'ont pas été faits au hasard. Ils suivent un schéma logique, le même que celui que vous voudrez utiliser dans la plupart de vos choix.

  1. Comme avec n'importe quel argument de fonction, si vous avez seulement besoin de lire l'argument sans le changer, choisissez plutôt de le passer comme une référence const. Les opératons arithmétiques ordinaires (comme + et -, etc.) et les booléens ne changeront pas leurs arguments, aussi, les passer par référence const est la technique que vous utiliserez dans la plupart des cas. Lorsque la fonction est un membre de classe, ceci revient à en faire une fonction membre const. Seulement avec les opérateurs d'affectation (comme +=) et operator=, qui changent l'argument de gauche, l'argument de gauche n'est pas une constante, mais il est toujours passé par adresse parce qu'il sera modifié.
  2. Le type de la valeur de retour que vous devez choisir dépend de la signification attendue pour l'opérateur. (Je le répète, vous pouvez faire tout ce que vous voulez avec les arguments et les valeurs de retour.) Si l'effet de l'opérateur consiste à générer une nouvelle valeur, il se peut que vous ayez besoin de générer un nouvel objet comme valeur de retour. Par exemple, Integer::operator+ doit produire un objet Integer qui est la somme des opérandes. Cet objet est retourné par valeur comme un const, ainsi la résultat ne peut être modifié comme 'lvalue'.
  3. Tous les opérateurs d'affectation modifient la 'lvalue'. Pour permettre au résultat de l'affectation d'être utilisé dans des expressions chaînées, comme a=b=c, on s'attend à ce que vous retourniez une référence identique à cette même 'lvalue' qui vient d'être modifiée. Mais cette référence doit-elle être const ou non const? Bien que vous lisiez a=b=c de gauche à droite, le compilateur l'analyse de droite à gauche, de sorte que vous n'êtes pas forcé de retourner un non const pour supporter l'affectation en chaîne. Toutefois, les gens s'attendent parfois à être capables d'effectuer une opération sur la chose venant juste d'être affectée, comme (a=b).func( ); pour appeler func( ) sur a après affectation de b sur lui. Dans ce cas la valeur de retour pour tous les opérateurs d'affectation devrait être une référence non const à la 'lvalue'.
  4. Pour les opérateurs logiques, tout le monde s'attend à récupérer au pire un int, et au mieux un bool. (Les librairies développées avant que la plupart des compilateurs ne supportent le type prédéfini C++'s bool utiliseront int ou un typedef équivalent.)

Les opérateurs d'incrémentaton et de décrémentation sont la source d'un dilemne à cause des versions préfixée et postfixée. Les deux versions modifient l'objet, et ne peuvent, de la sorte, traiter l'objet comme un const. La version préfixée retourne la valeur de l'objet après qu'il eut été changé, vous espérez donc récupérer l'objet ayant été modifié. Ainsi, avec la version préfixée vous pouvez simplement retourner *this en tant que référence. La version postfixée est supposée retourner la valeur avant qu'elle ne soit changée, aussi vous êtes obligés de créer un objet séparé pour représenter cette valeur et de le retourner. Ainsi avec la version postfixée, vous devez retourner par valeur si vous voulez préserver la sémantique attendue (Notez que vous trouverez parfois les opérateurs d'incrémentation et de décrémentation renvoyant un int ou un bool pour indiquer, par exemple, si un objet conçu pour se déplacer à l'intérieur d'une liste est parvenu à la fin de cette liste). Maintenant la question est : ces valeurs doivent elles être renvoyées const ou non const? Si vous donnez la permission de modifier l'objet et que quelqu'un écrit (++a).func( ), func( ) travaillera sur a lui-même, mais avec (a++).func( ), func( ) travaille sur l'objet temporaire retourné par l'opérateur postfixé operator++. Les objets temporaires sont automatiquement const, de sorte que ceci sera repéré par le compilateur, mais pour des raisons de cohérence il est plus logique de les rendre tous deux const, comme ce qui a été fait ici. Sinon vous pouvez choisir de rendre la version préfixée non- const et la postfixée const. A cause de la variété des significations que vous pouvez vouloir donner aux opérateurs d'incrément et de décrément, il faut faire une étude au cas par cas.

Retour par valeur en tant que const

Retourner par valeur en const peut sembler, à première vue, un peu subtil, cela mérite donc un peu plus d'explications. Considerez l'opérateur binaire operator+. Si vous l'utilisez dans une expression telle que f(a+b), le résultat de a+b devient un objet temporaire qui est utilisé dans l'appel à f( ). Parce qu'il est temporaire, il est automatiquement const, ainsi le fait de rendre la valeur de retour explicitement const ou le contraire n'a aucun effet.

Cependant, il vous est aussi possible d'envoyer un message à la valeur de retour de a+b, plutôt que de la passer en argument à la fonction. Par exemple, vous pouvez dire (a+b).g( ), où g( ) est quelque fonction membre de Integer dans ce cas. En rendant la valeur de retour const, vous stipulez que seule une fonction membre const peut être appelée pour cette valeur de retour. C'est ' const-correct', parce que cela vous empêche de stocker une information potentiellement utile dans un objet qui sera selon toute vraisemblance perdu.

L'optimisation de la valeur de retour

Lorsque de nouveaux objets sont créés pour un retour par valeur, remarquez la forme utilisée. Dans operator+, par exemple:

 
Sélectionnez
return Integer(left.i + right.i);

Ceci peut ressembler, à première vue, à, un “appel à un constructeur”, mais ce n'en est pas un. La syntaxe est celle d'un objet temporaire ; l'instruction dit “fabrique un objet Integer temporaire et retourne le.” A cause de cela, vous pourriez penser que le résultat est le même que de créer un objet local nommé et le retourner. Cependant, c'est tout à fait différent. Si au lieu de cela vous deviez dire :

 
Sélectionnez
Integer tmp(left.i + right.i);
return tmp;

Il se passerait trois choses. D'abord, l'objet tmp est créé incluant un appel à son constructeur. Ensuite, le constructeur de copie reproduit tmp à l'emplacement de la valeur de retour vers l'appelant. Troisièmement, le destructeur est appelé pour tmp à la fin de la portée.

Au contraire, l'approche “retourner un temporaire” opère de façon sensiblement différente. Quand le compilateur vous voit faire cela, il sait que vous n'avez pas de besoin ultérieur de l'objet qu'il crée autre que le renvoyer. Le compilateur tire avantage de celà en bâtissant l'objet directement à l'endroit de la valeur de retour vers l'appelant. Ceci ne nécessite qu'un simple appel de constructeur (pas besoin du constructeur de copie) et il n'y a pas d'appel de destructeur parce que vous ne crééz jamais effectivement un objet local. Ainsi, alors que cela ne coûte rien de plus que la compétence du programmeur, c'est sensiblement plus efficace. On appelle souvent cela optimisation de la valeur de retour.

12.3.4. opérateurs inhabituels

Plusieurs autres opérateurs ont une syntaxe sensiblement différente pour la surcharge .

L'opérateur d'indexation, operator[ ], doit être une fonction membre et il nécessite un argument unique. Parce que operator[ ] implique que l'objet pour lequel il est appelé agisse comme un tableau, vous ferez souvent retourner une référence par cet opérateur, de façon qu'il puisse être facilement utilisé comme partie gauche d'une affectation. Cet opérateur est fréquemment surchargé ; vous verrez des exemples dans le reste de l'ouvrage.

Les opérateurs new et delete contrôlent l'allocation dynamique de mémoire et peuvent être surchargés d'un grand nombre de manières. Ce sujet est couvert dans le chapitre 13.

Opérateur virgule

L'opérateur virgule est appelé lorsqu'il apparaît à côté d'un objet du type pour lequel la virgule est définie. Toutefois, “ operator,n'est pas appelé pour des listes d'arguments de fonctions, seulement pour des objets qui sont à l'extérieur, séparés par des virgules. Il ne semble pas y avoir beaucoup d'utilisations pratiques de cet opérateur; il existe dans le langage pour des raisons de cohérence. Voici un exemple montrant comment l'opérateur virgule peut être appelé quand la virgule apparaît avant un objet, aussi bien qu'après :

 
Sélectionnez
//: C12:OverloadingOperatorComma.cpp
#include <iostream>
using namespace std;
 
class After {
public:
  const After& operator,(const After&) const {
    cout << "After::operator,()" << endl;
    return *this;
  }
};
 
class Before {};
 
Before& operator,(int, Before& b) {
  cout << "Before::operator,()" << endl;
  return b;
}
 
int main() {
  After a, b;
  a, b;  // Appel de l'opérateur virgule
 
  Before c;
  1, c;  // Appel de l'opérateur virgule
} ///:~

La fonction globale permet à la virgule d'être placée avant l'objet en question. L'usage montré est relativement obscur et discutable. Bien que vous puissiez probablement utiliser une liste séparée par des virgules comme partie d'une expression plus complexe, c'est trop subtil à utiliser dans la plupart des cas.

Operator->

L' opérateur-> est généralement utilisé quand vous voulez faire apparaître un objet comme un pointeur. Etant donné qu'un tel objet comporte plus de “subtilités” qu'il n'en existe pour un pointeur usuel, un tel objet est souvent appelé un pointeur intelligent. Ils sont particulièrement utiles si vous voulez “envelopper” une classe autour d'un pointeur pour rendre ce pointeur sécurisé, ou bien dans le rôle habituel d'un itérateur, qui est un objet générique qui se déplace dans une collection / conteneur d'autres objets et les choisit un par un, sans procurer d'accès direct à l'implémentation du conteneur (Vous trouverez souvent des conteneurs et des itérateurs dans les librairies de classes, comme dans la Librairie Standard C++, décrite dans le Volume 2 de ce livre).

Un opérateur de déréférencement de pointeur doit être une fonction membre. Il possède des contraintes supplémentaires atypiques : Il doit retourner un objet (ou une référerence à un objet) qui possède aussi un opérateur de déréférencement de pointeur, ou il doit retourner un pointeur qui peut être utilisé pour sélectionner ce que la flèche de déréférencement de pointeur pointe. Voici un exemple simple:

 
Sélectionnez
//: C12:SmartPointer.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std;
 
class Obj {
  static int i, j;
public:
  void f() const { cout << i++ << endl; }
  void g() const { cout << j++ << endl; }
};
 
// définitions de membres statiques :
int Obj::i = 47;
int Obj::j = 11;
 
// Conteneur :
class ObjContainer {
  vector<Obj*> a;
public:
  void add(Obj* obj) { a.push_back(obj); }
  friend class SmartPointer;
};
 
class SmartPointer {
  ObjContainer& oc;
  int index;
public:
  SmartPointer(ObjContainer& objc) : oc(objc) {
    index = 0;
  }
  // La valeur de retour indique la fin de liste:
  bool operator++() { // Préfixé
    if(index >= oc.a.size()) return false;
    if(oc.a[++index] == 0) return false;
    return true;
  }
  bool operator++(int) { // Postfixé
    return operator++(); // Utilise la version préfixée
  }
  Obj* operator->() const {
    require(oc.a[index] != 0, "Valeur nulle "
      "renvoyée par SmartPointer::operator->()");
    return oc.a[index];
  }
};
 
int main() {
  const int sz = 10;
  Obj o[sz];
  ObjContainer oc;
  for(int i = 0; i < sz; i++)
    oc.add(&o[i]); // Le remplit
  SmartPointer sp(oc); // Crée un itérateur
  do {
    sp->f(); // Appel de déréférencement de pointeur
    sp->g();
  } while(sp++);
} ///:~

La classe Obj définit les objets qui sont manipulés par le programme. Les fonctions f( ) et g( ) ne font qu'afficher les valeurs intéressantes des données membres static. Les pointeurs vers ces objets sont stockés dans des conteneurs du type ObjContainer en utilisant sa fonction add( ). ObjContainer ressemble à un tableau de pointeurs, mais vous remarquerez qu'il n'existe aucun moyen d'en retirer les pointeurs. Toutefois, SmartPointer est déclarée comme une classe friend, de sorte qu'elle a le droit de regarder à l'intérieur du conteneur. La classe SmartPointer class ressemble beaucoup à un pointeur intelligent - vous pouvez le déplacer en utilisant operator++(vous pouvez également définir un operator- -), il n'ira pas au delà du conteneur dans lequel il pointe, et il restitue (au moyen de l'opérateur de déréférencement) la valeur vers laquelle il pointe. Notez que SmartPointer est une spécialisation pour le conteneur pour lequel il est créé ; à la différence d'un pointeur ordinaire, il n'existe aucun pointeur intelligent “à tout faire”. Vous en aprendrez plus sur les pointeurs intelligents appelés “itérateurs” dans le dernier chapitre de ce livre et dans le Volume 2 (téléchargeable depuis www.BruceEckel.com).

Dans main( ), un fois que le conteneur oc est rempli avec des objets Obj, un SmartPointer sp est créé. Les appels au pointeur intelligent surviennent dans les expressions :

 
Sélectionnez
sp->f(); // appels au pointeur intelligent
sp->g(); 

Ici, même si sp n'a pas, en fait de fonctions membres f( ) et g( ), l'opérateur de déréférencement appelle automatiquement ces fonctions pour le Obj* qui est retourné par SmartPointer::operator->. Le compilateur effectue tout le travail de vérification pour s'assurer que l'appel de fonction marche correctement.

Bien que la mécanique sous-jacente de l'opérateur de déréférencement de pointeur soit plus complexe que pour les autres opérateurs, le but est exactement le même: procurer une syntaxe plus pratique pour les utilisateurs de vos classes

Un itérateur imbriqué

Il est plus commun de voir une classe “smart pointer” ou “iterator” imbriquée dans la classe qu'elle sert. L'exemple précédent peut être réécrit pour imbriquer SmartPointer à l'intérieur de ObjContainer comme ceci :

 
Sélectionnez
//: C12:NestedSmartPointer.cpp
#include <iostream>
#include <vector>
#include "../require.h"
using namespace std;
 
class Obj {
  static int i, j;
public:
  void f() { cout << i++ << endl; }
  void g() { cout << j++ << endl; }
};
 
// définitions des membres statiques :
int Obj::i = 47;
int Obj::j = 11;
 
// Conteneur:
class ObjContainer {
  vector<Obj*> a;
public:
  void add(Obj* obj) { a.push_back(obj); }
  class SmartPointer;
  friend class SmartPointer;
  class SmartPointer {
    ObjContainer& oc;
    unsigned int index;
  public:
    SmartPointer(ObjContainer& objc) : oc(objc) {
      index = 0;
    }
    // La valeur de retour signale la fin de la liste :
    bool operator++() { // Préfixé
      if(index >= oc.a.size()) return false;
      if(oc.a[++index] == 0) return false;
      return true;
    }
    bool operator++(int) { // Postfixé
      return operator++(); // Utilise la version préfixée 
    }
    Obj* operator->() const {
      require(oc.a[index] != 0, "valeur Zéro "
        "renvoyée par SmartPointer::operator->()");
      return oc.a[index];
    }
  };
  // Fonction qui fournit un pointeur intelligent
  // pointant au début de l'ObjContainer:
  SmartPointer begin() { 
    return SmartPointer(*this);
  }
};
 
int main() {
  const int sz = 10;
  Obj o[sz];
  ObjContainer oc;
  for(int i = 0; i < sz; i++)
    oc.add(&o[i]); // Remplissage
  ObjContainer::SmartPointer sp = oc.begin();
  do {
    sp->f(); // Appel à l'opérateur de déréférencement
    sp->g();
  } while(++sp);
} ///:~

A côté de l'imbrication effective de la classe, il y a ici deux différences. La première est dans la déclaration de la classe de sorte qu'elle puisse être amie:

 
Sélectionnez
class SmartPointer;
friend SmartPointer;

Le compilateur doit d'abord savoir que la classe existe avant de pouvoir lui dire que c'est une amie.

La seconde différence est dans la fonction membre begin( ) de ObjContainer, qui fournit un SmartPointer qui pointe au début de la suite ObjContainer. Bien que ce soit en fait seulement une disposition d'ordre pratique, cela a une valeur universelle parce qu'elle se conforme à la règle utilisée dans la Librairie Standard C++.

Operator->*

L' opérateur->* est un opérateur binaire qui se comporte commme tous les autres opérateurs binaires. Il est proposé pour les situations où vous voulez imiter le comportement induit par la syntaxe pointeur sur membre, décrite dans le chapitre précédent.

Tout comme operator->, l'opérateur de déréférencement pointeur sur membre est généralement utilisé avec un type d'objet qui représente un “pointeur intelligent,” bien que l'exemple montré ici soit plus simple de façon à être compréhensible. L'astuce en définissant operator->* est qu'il doit retourner un objet pour lequel operator( ) puisse être appelé avec les arguments de la fonction membre que vous appelez.

L' opérateur d'appel de fonctionoperator( ) doit être une fonction membre, et il est unique en ce qu'il permet un nombre quelconque d'arguments. Il fait en sorte que votre objet ressemble effectivement à une fonction. Bien que vous puissiez définir plusieurs fonctions operator( ) avec différents arguments, il est souvent utilisé pour des types ayant une seule opération, ou au moins une particulièrement dominante. Vous verrez dans le volume 2 que la Librairie Standard C++ utilise l'opérateur d'appel de fonction afin de créer des “objets fonction.”

Pour créer un operator->* vous devez commencer par créer une classe avec un operator( ) qui est le type d'objet que operator->* retournera. Cette classe doit, d'une manière ou d'une autre, encapsuler l'information nécessaire pour que quand l' operator( ) est appelé (ce qui se produit automatiquement), le pointeur-sur-membre soit déréférencé pour l'objet. Dans l'exemple suivant, le constructeur FunctionObject capture et stocke à la fois le pointeur sur l'objet et le pointeur sur la fonction membre, et alors l' operator( ) utilise ceux-ci pour faire l'appel effectif pointeur-sur-membre

 
Sélectionnez
//: C12:PointerToMemberOperator.cpp
#include <iostream>
using namespace std;
 
class Dog {
public:
  int run(int i) const { 
    cout << "court\n";  
    return i; 
  }
  int eat(int i) const { 
     cout << "mange\n";  
     return i; 
  }
  int sleep(int i) const { 
    cout << "ZZZ\n"; 
    return i; 
  }
  typedef int (Dog::*PMF)(int) const;
  // l'opérateur->* doit retourner un objet 
  // ayant un operator():
  class FunctionObject {
    Dog* ptr;
    PMF pmem;
  public:
    // Enregistrer le pointeur sur objet et le pointeur sur membre
    FunctionObject(Dog* wp, PMF pmf) 
      : ptr(wp), pmem(pmf) { 
      cout << "constructeur FunctionObject\n";
    }
    // Faire l'appel en utilisant le pointeur sur objet
    // et le pointeur membre
    int operator()(int i) const {
      cout << "FunctionObject::operator()\n";
      return (ptr->*pmem)(i); // Faire l'appel
    }
  };
  FunctionObject operator->*(PMF pmf) { 
    cout << "operator->*" << endl;
    return FunctionObject(this, pmf);
  }
};
 
int main() {
  Dog w;
  Dog::PMF pmf = &Dog::run;
  cout << (w->*pmf)(1) << endl;
  pmf = &Dog::sleep;
  cout << (w->*pmf)(2) << endl;
  pmf = &Dog::eat;
  cout << (w->*pmf)(3) << endl;
} ///:~

Dog a trois fonctions membres, chacune d'elles prend un argument int et retourne un int. PMF est un typedef pour simplifier la définition d'un pointeur-sur-membre sur les fonctions membres de Dog.

Un FunctionObject est créé et retourné par operator->*. Notez que operator->* connaît à la fois l'objet pour lequel le pointeur-sur-membre est appelé ( this) et le pointeur-sur-membre, et il les passe au constructeur de FunctionObject qui conserve les valeurs. Lorsque operator->* est appelé, le compilateur le contourne immédiatement et appelle operator( ) pour la valeur de retour de operator->*, en passant les arguments qui étaient donnés à operator->*. Le FunctionObject::operator( ) prend les arguments et ensuite déréférence le pointeur-sur-membre “reél” en utilisant les pointeurs sur objet et pointeur-sur-membre enregistrés.

Remarquez que ce que vous êtes en train de faire là, tout comme avec operator->, consiste à vous insérer au beau milieu de l'appel à operator->*. Ceci vous donne la possibilité d'accomplir certaines opératons supplémentaires si le besoin s'en fait sentir.

Le mécanisme de l' operator->* implémenté ici ne fonctionne que pour les fonctions membres prenant un argument int et retournant un int. C'est une limitation, mais si vous essayez de créer des mécanismes surchargés pour chaque possibilité différente, cela paraît une tâche prohibitive. Heureusement, le mécanisme template de C++ (décrit dans le dernier chapitre de ce livre, et dans le Volume 2) est conçu pour traiter exactement ce genre de problème.

12.3.5. Opérateurs que vous ne pouvez pas surcharger

Il y a certains opérateurs, dans le jeu disponible, qui ne peuvent être surchargés. La raison générale invoquée pour cela est la sécurité. Si ces opérateurs étaient surchargeables, cela saboterait ou réduirait à néant les mécanismes de sécurité, rendraient les choses plus difficiles, ou jetterait le trouble sur les pratiques existantes.

  • L'opérateur de sélection de membre operator.. Actuellement, le point a une signification pour tout membre d'une classe, mais si vous permettez qu'on le surcharge, alors il ne vous serait plus possible d'accéder aux membres de la façon habituelle ; Il vous faudrait à la place un pointeur et la flèche operator->.
  • Le déréférencement de pointeur-sur-membre operator.*, pour les mêmes raisons que operator..
  • Il n'y a pas d'opérateur d'exponentiation. Le candidat le plus populaire pour ça était operator** deFortran, mais cela soulevait des questions difficiles pour l'analyseur syntaxique. C'est pourquoi, C n'a pas d'opérateur d'exponentiation, et de même C++ ne semble pas en avoir besoin lui-même parce que vous pouvez toujours effectuer un appel de fonction. Un opérateur d'exponentiation ajouterait une notation pratique, mais aucune nouvelle fonctionnalité en rapport avec la nouvelle complexité induite pour le compilateur.
  • Il n'y a pas d'opérateurs définis par l'utilisateur. Ce qui signifie que vous ne pouvez créer de nouveaux opérateurs qui ne sont pas dans le jeu standard. Une partie du problème réside dans la détermination des règles de priorité, une autre dans un un besoin insuffisant au vu du dérangement introduit.
  • Vous ne pouvez changer les règles de priorité. Elles sont assez difficiles comme ça, pour ne pas laisser les gens jouer avec.

12.4. Opérateurs non membres

Dans quelques uns des exemples précédents, les opérateurs peuvent être membres ou non membres, et cela ne semble pas faire une grande différence. Cela soulève habituellement la question, “Lequel dois-je choisir?” En général, si cela ne fait aucune différence, il faudrait opter pour les membres, pour mettre en relief l'association entre l'opérateur et sa classe. Lorsque le membre de gauche est toujours un objet de la classe courante, cela marche bien.

Cependant, quelquefois vous voulez que l'opérande de gauche soit un objet de quelque autre classe. Un endroit courant où vous verrez cela est avec les opérateurs << et >> surchargés pour les iostreams. Etant donné que les iostreams constituent une librairie C++ fondamentale, vous voudrez certainement surcharger ces opérateurs pour la plupart de vos classes, aussi cela vaut la peine de mémoriser le processus :

 
Sélectionnez
//: C12:IostreamOperatorOverloading.cpp
// Example of non-member overloaded operators
#include "../require.h"
#include <iostream>
#include <sstream> // "flux de chaînes"
#include <cstring>
using namespace std;
 
class IntArray {
  enum { sz = 5 };
  int i[sz];
public:
  IntArray() { memset(i, 0, sz* sizeof(*i)); }
  int& operator[](int x) {
    require(x >= 0 && x < sz,
      "IntArray::operator[] index hors limites");
    return i[x];
  }
  friend ostream&
    operator<<(ostream& os, const IntArray& ia);
  friend istream&
    operator>>(istream& is, IntArray& ia);
};
 
ostream& 
operator<<(ostream& os, const IntArray& ia) {
  for(int j = 0; j < ia.sz; j++) {
    os << ia.i[j];
    if(j != ia.sz -1)
      os << ", ";
  }
  os << endl;
  return os;
}
 
istream& operator>>(istream& is, IntArray& ia){
  for(int j = 0; j < ia.sz; j++)
    is >> ia.i[j];
  return is;
}
 
int main() {
  stringstream input("47 34 56 92 103");
  IntArray I;
  input >> I;
  I[4] = -1; // Utilise l'operator[] surchargé
  cout << I;
} ///:~

Cette classe contient aussi un opérateur [ ], qui retourne une référence vers une valeur valide du tableau. Parce qu'on retourne une référence, l'expression

 
Sélectionnez
I[4] = -1;

non seulement a l'air plus civilisée que si on utilisait des pointeurs, mais elle accomplit aussi l'effet désiré.

Il est important que les opérateurs de décalage surchargés passent et retournent par référence, de sorte que les actions affectent les objets externes. Dans les définitions de fonctions, des expressions comme

 
Sélectionnez
os << ia.i[j];

ont pour effet que les fonctions surchargées d'opérateurs existantes sont appelées (c'est à dire, celles définies dans <iostream>). Dans ce cas, la fonction appelée est ostream& operator<<(ostream&, int) parce que ia.i[j] est résolu comme un int.

Une fois que toutes les actions sont effectuées sur le istream ou le ostream, il est retourné, de sorte qu'il peut être réutilisé dans une expression plus complexe.

Dans main( ), un nouveau type de iostream est utilisé : le stringstream(déclaré dans <sstream>). C'est une classe qui prend un string(qu'il peut créer à partir d'un tableau de char, comme c'est montré ici) et le transforme en un iostream. Dans l'exemple ci-dessus, cela signifie que les opérateurs de décalage peuvent être testés sans ouvrir un fichier ou taper des données en ligne de commande.

La forme montrée dans cet exemple pour l'inserteur et l'extracteur est standard. Si vous voulez créer ces opérateurs pour votre propre classe, copiez les signatures des fonctions ainsi que les types de retour ci-dessus et suivez la forme du corps.

12.4.1. Conseils élémentaires

Murray (49)suggère ces directives pour choisir entre membres et non membres :

Opérateur Usage recommandé
Tous les opérateurs unaires membre
= ( ) [ ] -> ->* doit être membre
+= -= /= *= ^= &= |= %= >>= <<= membre
Tous les autres opérateurs binaires non membre

12.5. Surcharge de l'affectation

Une source habituelle de confusion pour les programmeurs C++ novices est l'affectation. C'est sans doute parce que le signe = est une opération aussi fondamentale en programmation, que de copier un registre au niveau machine. De plus, le constructeur de copie (décrit dans le Chapitre 11) est aussi quelquefois invoqué quand le symbole = est utilisé :

 
Sélectionnez
MyType b;
MyType a = b;
a = b;

Dans la seconde ligne, l'objet a est défini. Un nouvel objet est créé là où aucun n'existait auparavant. Parce que vous savez maintenant combien le compilateur C++ est sourcilleux pour ce qui concerne l'initialisation d'objets, vous savez qu'un constructeur doit toujours être appelé à l'endroit où un objet est défini. Mais quel constructeur? a est créé à partir d'un objet MyType existant ( b, à droite du signe égal), de sorte qu'il n'y a qu'un seul choix: le constructeur de copie. Quand bien même un signe égal est invoqué, le constructeur de copie est appelé.

Dans la troisième ligne, les choses sont différentes. A gauche du signe égal, il y a un objet ayant déjà été initialisé précédemment. Il est clair que vous n'appelez pas un constructeur pour un objet ayant déjà été créé. Dans ce cas MyType::operator= est appelé pour un a, prenant pour argument tout qui se peut se trouver à droite. (Vous pouvez avoir plusieurs fonctions operator= prenant différents types d'arguments de droite.)

Ce comportement n'est pas réservé au constructeur de copie. A chaque fois que vous initialisez un objet en utilisant = au lieu de l'appel ordinaire sous forme de fonction d'un constructeur, le compilateur va chercher un constructeur qui accepte ce qui se trouve à droite.

 
Sélectionnez
//: C12:CopyingVsInitialization.cpp
class Fi {
public:
  Fi() {}
};
 
class Fee {
public:
  Fee(int) {}
  Fee(const Fi&) {}
};
 
int main() {
  Fee fee = 1; // Fee(int)
  Fi fi;
  Fee fum = fi; // Fee(Fi)
} ///:~

Lorsqu'on utilise le signe =, il est important de conserver à l'esprit cette distinction : Si l'objet n'a pas encore été créé, l'initialisation est nécessaire ; autrement l'opérateur d'affectation operator= est utilisé.

Il est même préférable d'éviter d'écrire du code qui utilise le = pour l'initialisation ; au lieu de cela, utilisez toujours la forme explicite du constructeur. Les deux constructions avec le signe = deviennent alors :

 
Sélectionnez
Fee fee(1);
Fee fum(fi);

De la sorte vous éviterez de semer la confusion chez vos lecteurs.

12.5.1. Comportement de operator=

Dans Integer.h et Byte.h, vous avez vu que operator= ne peut être qu'une fonction membre. Il est intimement connecté à l'objet qui se trouve à gauche de ‘ ='. S'il était possible de définir operator= globalement, alors vous pourriez essayer de redéfinir le signe ‘ =' prédéfini :

 
Sélectionnez
int operator=(int, MyType); // Global = interdit!

Le compilateur contourne entièrement ce problème en vous forçant à faire de operator= une fonction membre.

Lorsque vous créez un operator=, vous devez copier toute l'information nécessaire de l'objet de droite dans l'objet courant (C'est à dire, l'objet sur lequel l' operator= est appelé) pour accomplir tout ce que vous considérez une “affectation” pour votre classe. Pour des objets simples, c'est évident:

 
Sélectionnez
//: C12:SimpleAssignment.cpp
// Simple operator=()
#include <iostream>
using namespace std;
 
class Value {
  int a, b;
  float c;
public:
  Value(int aa = 0, int bb = 0, float cc = 0.0)
    : a(aa), b(bb), c(cc) {}
  Value& operator=(const Value& rv) {
    a = rv.a;
    b = rv.b;
    c = rv.c;
    return *this;
  }
  friend ostream&
  operator<<(ostream& os, const Value& rv) {
    return os << "a = " << rv.a << ", b = "
      << rv.b << ", c = " << rv.c;
  }
};
 
int main() {
  Value a, b(1, 2, 3.3);
  cout << "a: " << a << endl;
  cout << "b: " << b << endl;
  a = b;
  cout << "a après affectation: " << a << endl;
} ///:~

Ici l'objet à la gauche du signe = copie tous les éléments de l'objet de droite, puis retourne une référence sur lui-même, ce qui permet la création d'une expression plus complète.

Cet exemple comporte une erreur commune. Lorsque vous affectez deux objets du même type, vous devez toujours commencer par vérifier l'auto-affectation : l'objet est-il affecté à lui-même ? Dans certains cas, comme celui-ci, c'est sans danger de réaliser cette affectation tout de même, mais si des changements sont faits à l'implémentation de la classe, cela peut faire une différence, et si vous ne le faites pas de manière routinière, vous pouvez l'oublier et provoquer des bogues difficiles à identifier.

Les pointeurs dans les classes

Que se passe-t-il si l'objet n'est pas si simple ? Par exemple, que se passe-t-il si l'objet contient des pointeurs vers d'autres objets ? Le fait de simplement copier un pointeur signifie que vous vous retrouverez avec deux objets pointant sur un même emplacement mémoire. Dans des situations de ce genre, il vous faut faire votre propre comptabilité.

Il y a deux approches communes à ce problème. La technique la plus simple consiste à recopier tout ce que le pointeur référence lorsque vous faites une affectation ou une construction par copie. C'est immédiat :

 
Sélectionnez
//: C12:CopyingWithPointers.cpp
// Résolution du problème d'aliasing des pointeurs
// en dupliquant ce qui est pointé durant
// l'affectation et la construction par copie.
#include "../require.h"
#include <string>
#include <iostream>
using namespace std;
 
class Dog {
  string nm;
public:
  Dog(const string& name) : nm(name) {
    cout << "Creation de Dog: " << *this << endl;
  }
  // Le constructeur de copie & l'operator= 
  // synthétisés sont corrects.
  // création d'un Dog depuis un pointeur sur un Dog :
  Dog(const Dog* dp, const string& msg) 
    : nm(dp->nm + msg) {
    cout << "Copie d'un dog " << *this << " depuis "
         << *dp << endl;
  }
  ~Dog() { 
    cout << "Destruction du Dog: " << *this << endl;
  }
  void rename(const string& newName) {
    nm = newName;
    cout << "Dog renommé : " << *this << endl;
  }
  friend ostream&
  operator<<(ostream& os, const Dog& d) {
    return os << "[" << d.nm << "]";
  }
};
 
class DogHouse {
  Dog* p;
  string houseName;
public:
  DogHouse(Dog* dog, const string& house)
   : p(dog), houseName(house) {}
  DogHouse(const DogHouse& dh)
    : p(new Dog(dh.p, " copie-construit")),
      houseName(dh.houseName 
        + " copie-construit") {}
  DogHouse& operator=(const DogHouse& dh) {
    // vérification de l'auto-affectation :
    if(&dh != this) {
      p = new Dog(dh.p, " affecté");
      houseName = dh.houseName + " affecté";
    }
    return *this;
  }
  void renameHouse(const string& newName) {
    houseName = newName;
  }
  Dog* getDog() const { return p; }
  ~DogHouse() { delete p; }
  friend ostream&
  operator<<(ostream& os, const DogHouse& dh) {
    return os << "[" << dh.houseName 
      << "] contient " << *dh.p;
  }
}; 
 
int main() {
  DogHouse fidos(new Dog("Fido"), "Niche de Fido");
  cout << fidos << endl;
  DogHouse fidos2 = fidos; // Construction par copie
  cout << fidos2 << endl;
  fidos2.getDog()->rename("Spot");
  fidos2.renameHouse("Niche de Spot");
  cout << fidos2 << endl;
  fidos = fidos2; // Affectation
  cout << fidos << endl;
  fidos.getDog()->rename("Max");
  fidos2.renameHouse("Niche de Max");
} ///:~

Dog est une classe simple qui ne contient qu'un string qui conserve le nom du chien. Cependant, vous serez généralement averti que quelque chose arrive à un Dog parce que les constructeurs et les destructeurs affichent des informations lorsqu'ils sont invoqués. Notez que le second constructeur est un peu comme un constructeur de copie excepté qu'il prend un pointeur sur un Dog au lieu d'une référence, et qu'il a un second argument qui est un message qui est concaténé à l'argument nom du Dog. Cela est utilisé pour aider à tracer le comportement du programme.

Vous pouvez voir qu'à chaque fois qu'une fonction membre affiche de l'information, elle n'accède pas à cette information directement mais au contraire envoie *this à cout. Ceci appelle ensuite ostream operator<<. C'est une bonne façon de faire parce que si vous voulez reformater la manière dont l'information de Dog est affichée (comme je l'ai fait en ajoutant ‘[' et ‘]') vous n'avez besoin de le faire qu'à un seul endroit.

Un DogHouse contient un Dog* et illustre les quatre fonctions qu'il vous faudra toujours définir quand votre classe contient des pointeurs : tous les constructeurs ordinaires nécessaires, le constructeur de copie, operator=(définissez-le ou interdisez-le), et un destructeur. L'opérateur operator= vérifie l'auto-affectation, cela va sans dire, même si ce n'est pas strictement nécessaire ici. Ceci élimine presque totalement la possibilité que vous puissiez oublier cette vérification si vous modifiez le code de sorte que ce point devienne important.

Le comptage de références

Dans l'exemple ci-dessus, le constructeur de copie et operator= font une nouvelle copie de ce que le pointeur référence, et le destructeur le libère. Toutefois, si votre objet nécessite beaucoup de mémoire ou bien un lourd travail d'initialisation, il se peut que vous ayez envie d'éviter cette copie. Une approche habituelle de ce problème est appelée comptage de références. Vous donnez de l'intelligence à l'objet pointé de sorte qu'il sache combien d'objets pointent sur lui. Alors construction par copie ou affectation signifient attacher un autre pointeur à un objet existant et incrémenter le compteur de références. La destruction signifie diminuer le compteur de référence et détruire l'objet si ce compteur atteint zéro.

Mais que se passe-t-il si vous voulez écrire dans l'objet(le Dog dans l'exemple précédent) ? Plus d'un objet peuvent utiliser ce Dog, de sorte que vous allez modifier le Dog de quelqu'un d'autre en même temps que le votre ce qui ne semble pas très amical. Pour résoudre ce problème d' “aliasing”, une technique supplémentaire appelée copie à l'écriture(copy-on-write) est utilisée. Avant d'écrire dans un bloc de mémoire, vous vous assurez que personne d'autre ne l'utilise. Si le compteur de références est supérieur à un, vous devez vous créer une copie personnelle de ce bloc avant d'écrire dessus, de la sorte vous ne piétinez les plates-bandes de personne. Voici un exemple simple de comptage de références et de copie à l'écriture :

 
Sélectionnez
//: C12:ReferenceCounting.cpp
// Comptage de références, copie à l'écriture
#include "../require.h"
#include <string>
#include <iostream>
using namespace std;
 
class Dog {
  string nm;
  int refcount;
  Dog(const string& name) 
    : nm(name), refcount(1) {
    cout << "Creation du Dog: " << *this << endl;
  }
  // Empêcher l'affectation
  Dog& operator=(const Dog& rv);
public:
  // Les Dog ne peuvent être créés que sur le tas :
  static Dog* make(const string& name) {
    return new Dog(name);
  }
  Dog(const Dog& d) 
    : nm(d.nm + " copie"), refcount(1) {
    cout << "Dog constructeur copie : " 
         << *this << endl;
  }
  ~Dog() { 
    cout << "Destruction du Dog : " << *this << endl;
  }
  void attach() { 
    ++refcount;
    cout << "Attachement du Dog : " << *this << endl;
  }
  void detach() {
    require(refcount != 0);
    cout << "Detachement du Dog : " << *this << endl;
    // Détruit l'objet si personne ne s'en sert:
    if(--refcount == 0) delete this;
  }
  // Copy conditionnelle de ce 'Dog'.
  // Appel avant de modifier, affectation
  // pointeur résultant vers votre Dog*.
  Dog* unalias() {
    cout << "Unaliasing du Dog: " << *this << endl;
    // Pas de duplication si pas d'alias:
    if(refcount == 1) return this;
    --refcount;
    // Utiliser le constructeur de copie pour dupliquer :
    return new Dog(*this);
  }
  void rename(const string& newName) {
    nm = newName;
    cout << "Dog renommé : " << *this << endl;
  }
  friend ostream&
  operator<<(ostream& os, const Dog& d) {
    return os << "[" << d.nm << "], rc = " 
      << d.refcount;
  }
};
 
class DogHouse {
  Dog* p;
  string houseName;
public:
  DogHouse(Dog* dog, const string& house)
   : p(dog), houseName(house) {
    cout << "Creation du DogHouse : "<< *this << endl;
  }
  DogHouse(const DogHouse& dh)
    : p(dh.p),
      houseName("copie construit " + 
        dh.houseName) {
    p->attach();
    cout << "DogHouse copie construction : "
         << *this << endl;
  }
  DogHouse& operator=(const DogHouse& dh) {
    // Vérification de l'auto-affectation:
    if(&dh != this) {
      houseName = dh.houseName + " affecté";
      // Nettoyer d'abord ce que vous utilisez:
      p->detach();
      p = dh.p; // Comme le constructeur de copie
      p->attach();
    }
    cout << "DogHouse operator= : "
         << *this << endl;
    return *this;
  }
  // Décrémente refcount, destruction ocnditionelle
  ~DogHouse() {
    cout << "DogHouse destructeur : " 
         << *this << endl;
    p->detach(); 
  }
  void renameHouse(const string& newName) {
    houseName = newName;
  }
  void unalias() { p = p->unalias(); }
  // Copie à l'écriture. A chaque fois que vous modifiez 
  // le contenu du pointeur vous devez d'abord 
  // faire en sorte qu'il ne soit plus partagé :
  void renameDog(const string& newName) {
    unalias();
    p->rename(newName);
  }
  // ... ou quand vous permettez l'accès à quelqu'un d'autre:
  Dog* getDog() {
    unalias();
    return p; 
  }
  friend ostream&
  operator<<(ostream& os, const DogHouse& dh) {
    return os << "[" << dh.houseName 
      << "] contient " << *dh.p;
  }
}; 
 
int main() {
  DogHouse 
    fidos(Dog::make("Fido"), "Niche de Fido"),
    spots(Dog::make("Spot"), "Niche de Spot");
  cout << "Avant copie construction" << endl;
  DogHouse bobs(fidos);
  cout << "Après copie construction de bobs" << endl;
  cout << "fidos:" << fidos << endl;
  cout << "spots:" << spots << endl;
  cout << "bobs:" << bobs << endl;
  cout << "Avant spots = fidos" << endl;
  spots = fidos;
  cout << "Après spots = fidos" << endl;
  cout << "spots:" << spots << endl;
  cout << "Avant l'auto affectation" << endl;
  bobs = bobs;
  cout << "Après l'auto affectation" << endl;
  cout << "bobs:" << bobs << endl;
  // Commentez les lignes suivantes :
  cout << "Avant rename("Bob")" << endl;
  bobs.getDog()->rename("Bob");
  cout << "Après rename("Bob")" << endl;
} ///:~

La classe Dog est l'objet pointé par un DogHouse. Il contient un compteur de références et des fonctions pour contrôler et lire le compteur de références. Un constructeur de copie est disponible de sorte que vous pouvez fabriquer un nouveau Dog à partir d'un autre existant déjà.

La fonction attach( ) incrémente le compteur de référence d'un Dog pour indiquer qu'un autre objet l'utilise. detach( ) décrémente le compteur de références. Si le compteur de références arrive à zéro, alors personne ne l'utilise plus, alors la fonction membre détruit son propre objet en disant delete this.

Avant de faire toute modification (comme renommer un Dog), vous devez vous assurer que vous ne modifiez pas un Dog qu'un autre objet utilise. Vous faites cela en appelant DogHouse::unalias( ), qui à son tour appelle Dog::unalias( ). Cette dernière fonction retournera le pointeur sur Dog existant si le compteur de référence vaut un (signifiant que personne d'autre ne pointe sur ce Dog), mais il dupliquera le Dog si le compteur de références est supérieur à un.

Le constructeur de copie, au lieu de créer sa propre zone mémoire, affecte Dog au Dog de l'objet source. Ensuite, parce qu'il y a maintenant un objet supplémentaire utilisant ce bloc de mémoire, il incrémente le compte de références en appelant Dog::attach( ).

L'opérateur operator= traite un objet ayant déjà été créé et qui se trouve à gauche de =, de sorte qu'il doit déjà nettoyer par un appel à detach( ) pour ce Dog, qui détruira l'ancien Dog si personne ne l'utilise. Ensuite operator= répète le comportement du constructeur de copie. Notez qu'il vérifie d'abord si vous affectez l'objet à lui-même.

Le destructeur appelle detach( ) pour détruire conditionelement le Dog.

Pour implémenter la copie à l'écriture, vous devez contrôler toutes les actions qui écrivent sur votre bloc de mémoire. Par exemple, la fonction membre renameDog( ) vous permet de changer les valeurs dans le bloc de mémoire. Mais d'abord, il utilise unalias( ) pour empêcher la modification d'un Dog'aliasé' (un Dog ayant plus d'un DogHouse pointant sur lui). Et si vous avez besoin de générer un pointeur vers un Dog depuis un DogHouse, vous appelez unalias( ) sur ce pointeur d'abord.

main( ) teste les différentes fonctions qui fonctionnent correctement pour implémenter le comptage de références : le constructeur, le constructeur de copie, operator=, et le destructeur. Il teste également la copie à l'écriture en appelant renameDog( ).

Voici la sortie (après une petite mise en forme):

 
Sélectionnez
Creation du Dog : [Fido], rc = 1
Creation du DogHouse : [FidoHouse] 
  contient [Fido], rc = 1
Creation du Dog: [Spot], rc = 1
Creation du DogHouse : [SpotHouse] 
  contient [Spot], rc = 1
Avant copie construction
Attachement du Dog : [Fido], rc = 2
DogHouse copie construction : 
  [copie construit FidoHouse] 
    contient [Fido], rc = 2
Après copie construction bobs
fidos:[FidoHouse] contient [Fido], rc = 2
spots:[SpotHouse] contient [Spot], rc = 1
bobs:[copie construit FidoHouse] 
  contient [Fido], rc = 2
Avant spots = fidos
Detachement du Dog : [Spot], rc = 1
Destruction du Dog : [Spot], rc = 0
Attachement du Dog : [Fido], rc = 3
DogHouse operator= : [FidoHouse affecté]
  contient [Fido], rc = 3
Après spots = fidos
spots:[FidoHouse affecté] contient [Fido],rc = 3
Avant auto affectation
DogHouse operator= : [copie construit FidoHouse]
  contient [Fido], rc = 3
Après auto affectation
bobs :[copie construit FidoHouse] 
  contient [Fido], rc = 3
avant rename("Bob")
après rename("Bob")
DogHouse destruction : [copie construit FidoHouse]
  contient [Fido], rc = 3
Detachement de Dog : [Fido], rc = 3
DogHouse destruction : [FidoHouse affecté] 
  contient [Fido], rc = 2
Detachement du Dog : [Fido], rc = 2
DogHouse destruction : [FidoHouse] 
  contient [Fido], rc = 1
Detachement du Dog : [Fido], rc = 1
Destruction du Dog: [Fido], rc = 0

En étudiant la sortie, en tracant dans le code source, et en vous livrant à des expériences à partir de ce programme, vous approfonderez votre compréhension de ces techniques.

creation automatique de l'operator=

Parce qu'affecter un objet à partir d'un autre objet du même type est une opération que la plupart des gens s'attendent à être possible, le compilateur créera automatiquement un type::operator=(type) si vous n'en créez pas un vous-même. Le comportement de cet opérateur imite celui du constructeur de copie créé automatiquement ; Si la classe contient des objets (ou dérive d'une autre classe), l'opérateur operator= pour ces objets est appelé récursvement. On appelle cela affectation membre à membre. Par exemple,

 
Sélectionnez
//: C12:AutomaticOperatorEquals.cpp
#include <iostream>
using namespace std;
 
class Cargo {
public:
  Cargo& operator=(const Cargo&) {
    cout << "dans Cargo::operator=()" << endl;
    return *this;
  }
};
 
class Truck {
  Cargo b;
};
 
int main() {
  Truck a, b;
  a = b; // Affiche: "dans Cargo::operator=()"
} ///:~

L'opérateur operator= généré automatiquement pour Truck appelle Cargo::operator=.

En général, vous ne voudrez pas laisser le compilateur faire cela pour vous. Avec des classes de toute complexité (spécialement si elles contiennent des pointeurs !) vous voudrez créer explicitement un operator=. Si vous ne voulez réellement pas que les gens effectuent des affectations, déclarez operator= comme une fonction private. (Vous n'avez pas besoin de le définir à moins que vous ne l'utilisiez dans la classe).

12.6. Conversion de type automatique

En C et en C++, si le compilateur voit une expression ou un appel de fonction utilisant un type n'est pas exactement celui qu'il attend, il peut souvent effectuer une conversion de type automatique du type qu'il a vers le type qu'il veut. En C++, vous pouvez provoquer ce même effet pour les types définis par l'utilisateur en définissant des fonctions de conversion automatique. Ces fonctions se présentent sous deux formes : un type particulier de constructeur et un opérateur surchargé.

12.6.1. Conversion par constructeur

Si vous définissez un constructeur qui prend comme seul argument un objet (ou une référence) d'un autre type, ce constructeur permet au compilateur d'effectuer une conversion de type automatique. Par exemple

 
Sélectionnez
//: C12:AutomaticTypeConversion.cpp
// Conversion de type par constructeur
class One {
public:
  One() {}
};
 
class Two {
public:
  Two(const One&) {}
};
 
void f(Two) {}
 
int main() {
  One one;
  f(one); // Veut un Two, utilise un One
} ///:~

Quand le compilateur voit f( ) appelé avec un objet de type One, il regarde la déclaration de f( ) et note qu'elle attend un Two. Ensuite il regarde s'il y a un moyen d'obtenir un Two partant d'un One, et trouve le constructeur Two::Two(One), qu'il appelle silencieusement. L'objet Two résultant est passé à f( ).

Dans ce cas, la conversion automatique de type vous a épargné la peine de définir deux versions surchargées de f( ). Cependant, le coût est l'appel caché du constructeur de Two, ce qui peut importer si vous êtes concerné par l'efficacité des appels de f( ).

Eviter la conversion par constructeur

Il y a des fois où la conversion automatique de type au travers du constructeur peut poser problèmes. Pour la désactiver, modifiez le constructeur en le préfaçant avec le mot clé explicit(qui ne fonctionne qu'avec les constructeurs). Utilisé pour modifier le constructeur de la classe Two dans l'exemple qui suit :

 
Sélectionnez
//: C12:ExplicitKeyword.cpp
// Using the "explicit" keyword
class One {
public:
  One() {}
};
 
class Two {
public:
  explicit Two(const One&) {}
};
 
void f(Two) {}
 
int main() {
  One one;
//!  f(one); // conversion automatique non autorisée
  f(Two(one)); // OK -- l'utilisateur effectue la conversion
} ///:~

En rendant le constructeur de Two explicite, le compilateur est prévenu qu'il ne doit pas effectuer la moindre conversion automatique en utilisant ce constructeur particulier (les autres constructeurs non- explicit dans cette classe peuvent toujours effectuer des conversions automatiques). Si l'utilisateur veut qu'une conversion survienne, le code doit être écrit. Dans le code ci-dessous, f(Two(one)) crée un objet temporaire de type Two à partir de one, exactement comme le faisait le compilateur dans la version précédente.

12.6.2. Conversion par opérateur

La seconde manière de produire une conversion de type automatique passe par la surcharge d'opérateur. Vous pouvez créer une fonction membre qui prend le type courant et le convertit dans le type désiré en utilisant le mot clé operator suivi par le type dans lequel vous voulez convertir. Cette forme de surcharge d'opérateur est unique parce que vous ne semblez pas spécifier de type de retour - Le type de retour est le nom de l'opérateur que vous surchargez. En voici un exemple :

 
Sélectionnez
//: C12:OperatorOverloadingConversion.cpp
class Three {
  int i;
public:
  Three(int ii = 0, int = 0) : i(ii) {}
};
 
class Four {
  int x;
public:
  Four(int xx) : x(xx) {}
  operator Three() const { return Three(x); }
};
 
void g(Three) {}
 
int main() {
  Four four(1);
  g(four);
  g(1);  // Appelle Three(1,0)
} ///:~

Avec la technique basée sur le constructeur, la classe de destination effectue la conversion, mais avec les opérateurs, c'est la classe source qui effectue la conversion. La valeur de la technique baséee sur le constructeur est que vous pouvez ajouter un moyen de conversion à un système existant quand vous créez une nouvelle classe. Cependant, la création d'un constructeur mono-argument définit toujours une conversionde type automatique (ainsi que s'il y a plus d'un argument, si le reste des arguments disposent de valeurs par défaut), qui peut ne pas être ce que vous voulez (dans ce cas, vous désactivez la conversion en utilisant explicit). De plus, il n'y a aucun moyen d'utiliser un constructeur de conversion d'un type utilisateur à un type intégré ; Ce n'est possible qu'avec la surcharge d'opérateur.

Réflexivité

L'une des raisons les plus commodes à l'utilisation d'opérateurs surchargés globaux par rapport aux opérateurs membres est que dans les versions globales, la conversion de type peut s'appliquer à l'un ou l'autre opérande, alors qu'avec les membres objet, l'opérande de gauche doit être du type adéquat. Si vous voulez convertir les deux opérandes, les versions globales peuvent économiser beaucoup de codage. Voici un petit exemple :

 
Sélectionnez
//: C12:ReflexivityInOverloading.cpp
class Number {
  int i;
public:
  Number(int ii = 0) : i(ii) {}
  const Number
  operator+(const Number& n) const {
    return Number(i + n.i);
  }
  friend const Number
    operator-(const Number&, const Number&);
};
 
const Number
  operator-(const Number& n1,
            const Number& n2) {
    return Number(n1.i - n2.i);
}
 
int main() {
  Number a(47), b(11);
  a + b; // OK
  a + 1; // le deuxième argument est converti en Number
//! 1 + a; // Mauvais ! le premier argument n'est pas de type Number
  a - b; // OK
  a - 1; // Le second argument est converti en Number
  1 - a; // Le premier argument est converti en Number
} ///:~

La classe Number a à la fois un operator+ et un operator- ami (ndt friend). Comme il y a un constructeur qui prend un seul argument int, un int peut être automatiquement converti en un Number, mais uniquement dans les bonnes conditions. Dans main( ), vous pouvez remarquer qu'ajouter un Number à un autre Number fonctionne correctement parce que cela correspond exactement à l'opérateur surchargé. De même, qund le compilateur voit un Number suivi d'un + et d'un int, il peut trouver la fonction membre Number::operator+ et convertir l'argument int en un Number en utilisant le constructeur. Mais quand il voit un int, un + et un Number, il ne sait pas que faire parce que tout ce qu'il a c'est Number::operator+ qui exoge que l'opérande de gauche soit déjà un objet Number. Le compilateur nous reporte donc une erreur.

Avec l' operator- friend, les choses sont différentes. Le compilateur doit remplir les arguments comme il peut ; il n'est pas restreint à avoir un Number comme argument de gauche. Auusi, si il voit

 
Sélectionnez
1 - a

il peut convertir le premier argument en Number en utilisant le constructeur.

Vous voulez parfois pouvoir restreindre l'utilisation de vos opérateurs en les rendant membres. Par exemple, quand vous multipliez une matrice par un vecteur, le vecteur doit aller à droite. Mais si vous voulez que vos opérateurs puissent convertir l'un ou l'autre argument, transformez l'opérateur en fonction amie.

Heureusement, le compilateur ne va pas prendre 1 - 1 et convertir chaque argument en objet Number et ensuite appeler l' operator-. Cela voudrait dire que le code C existant pourrait soudainement commencer à fonctionner différemment. Le compilateur sélectionne la possibilité “la plus simple” en premier, ce qui est l'opérateur intégré pour l'expression 1 - 1.

12.6.3. Exemple de conversion de type

Un exemple dans lequel la conversion automatique de type est extrèmement utile survient avec toute classe qui encapsule des chaînes de caractères (dans ce cas, nous allons juste implémenter la classe en utilisant la classe Standard C++ string parce que c'est simple). Sans conversion automatique de type, si vous voulez utiliser toutes les fonctions de chaîne de bibliothèque standard C, vous devez créer une fonction membre pour chacune, comme ceci :

 
Sélectionnez
//: C12:Strings1.cpp
// Pas de conversion de type automatique
#include "../require.h"
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std;
 
class Stringc {
  string s;
public:
  Stringc(const string& str = "") : s(str) {}
  int strcmp(const Stringc& S) const {
    return ::strcmp(s.c_str(), S.s.c_str());
  }
  // ... etc., pour chaque fonction dans string.h
};
 
int main() {
  Stringc s1("hello"), s2("there");
  s1.strcmp(s2);
} ///:~

Ici, seule la fonction strcmp( ) a été créé, mais vous auriez à créer un fonction correspondant à chacune de celles dans <cstring> qui pourrait s'avérer utile. Par chance, vous pouvez fournir une conversion automatique de type permettant d'accéder à toutes les fonction dans <cstring>:

 
Sélectionnez
//: C12:Strings2.cpp
// Avec conversion de type automatique
#include "../require.h"
#include <cstring>
#include <cstdlib>
#include <string>
using namespace std;
 
class Stringc {
  string s;
public:
  Stringc(const string& str = "") : s(str) {}
  operator const char*() const { 
    return s.c_str(); 
  }
};
 
int main() {
  Stringc s1("hello"), s2("there");
  strcmp(s1, s2); // fonction Standard C 
  strspn(s1, s2); // n'importe quelle fonction de chaîne !
} ///:~

Maintenant, toute fonction qui prend un argument char* peut aussi prendre un argument Stringc parce que le compilateur sait comment créer un char* depuis un Stringc.

12.6.4. Les pièges de la conversion de type automatique

Du fait que le compilateur doit choisir comment effectuer silencieusement une conversion de type, il peut être troublé si vous ne concevez pas correctement vos conversions. Une situation simple et évidente est celle qui survient avec une classe X qui peut se convertir en un objet de classe Y avec un operator Y( ). Si la classe Y dispose d'un constructeur prenant un seul argument de type X, cela représente la même conversion. Le compilateur a maintenant deux moyens d'aller de X à Y, donc il va générer une erreur d'ambiguité quand cette conversion se produit :

 
Sélectionnez
//: C12:TypeConversionAmbiguity.cpp
class Orange; // Class declaration
 
class Apple {
public:
  operator Orange() const; // Convertit Apple en Orange
};
 
class Orange {
public:
  Orange(Apple); // Convertit Apple en Orange
};
 
void f(Orange) {}
 
int main() {
  Apple a;
//! f(a); // Erreur : conversion ambiguë 
} ///:~

La solution évidente est de ne pas le faire. Fournissez simplement un moyen unique de convertir automatiquement un type en un autre.

Un problème plus difficile à résoudre apparait parfois quand vous fournissez des conversions automatiques vers plusieurs types. C'est parfois appelé fan-out:

 
Sélectionnez
//: C12:TypeConversionFanout.cpp
class Orange {};
class Pear {};
 
class Apple {
public:
  operator Orange() const;
  operator Pear() const;
};
 
// Overloaded eat():
void eat(Orange);
void eat(Pear);
 
int main() {
  Apple c;
//! eat(c);
  // Erreur : Apple -> Orange ou Apple -> Pear ???
} ///:~

La classe Apple peut être convertie automatiquement aussi bien en Orange qu'en Pear. Ce qui est incidieux ici est qu'il n'y a pas de problème jusqu'à ce que quelqu'un vienne innocemment créer deux versions surchargées de eat( ). (Avec une seule version, le code qui se trouve dans main( ) fonctionne bien.)

Encore une fois la solution - et le mot d'ordre général sur la conversion automatique de type - est de ne fournir qu'une seule conversion automatique d'un type vers un autre. Vous pouvez avoir des conversions vers d'autres types ; elles ne devraient simplement pas être automatiques. Vous pouvez créer des appels de fonction explicites avec des noms commes makeA( ) et makeB( ).

Les activités cachées

La conversion automatique de type peut introduire des activités plus fondamentales que vous pouvez l'envisager. Comme petite énigme, voyez cette modification de CopyingVsInitialization.cpp:

 
Sélectionnez
//: C12:CopyingVsInitialization2.cpp
class Fi {};
 
class Fee {
public:
  Fee(int) {}
  Fee(const Fi&) {}
};
 
class Fo {
  int i;
public:
  Fo(int x = 0) : i(x) {}
  operator Fee() const { return Fee(i); }
};
 
int main() {
  Fo fo;
  Fee fee = fo;
} ///:~

Il n'y a pas de constructeur pour créer Fee fee à partir d'un objet Fo. Cependant, Fo a une conversion automatique de type en Fee. Il n'y a pas de constructeur par recopie pour créer un Fee à partir d'un Fee, mais c'est l'une des fonctions spéciales que le compilateur peut créer pour vous. (le constructeur par défaut, le constructeur par recopie, l' operator= et le destructeur peuvent être automatiquement synthétisés par le compilateur.) Ainsi, pour la déclaration relativement innocente

 
Sélectionnez
Fee fee = fo;

l'opérateur de conversion automatique de type est appelé, et un constructeur par recopie est créé.

Utilisez la conversion automatique de type avec prudence. Comme pour toutes les surcharges d'opérateur, c'est excellent quand cela réduit le travail de codage de manière significative, mais cela ne vaut généralement pas la peine de l'utiliser gratuitement.

12.7. Résumé

L'unique raison de la surcharge d'opérateur réside dans les situations où elle rend la vie plus facile. Il n'y a rien de particulièrement magique à ce propos ; les opérateurs surchargés ne sont que des fonctions aux noms amusants, et les appels de fonctions sont faits pour vous par le compilateur lorsqu'il détecte la construction syntaxique associée. Mais si la surcharge d'opérateurs ne procure pas un bénéfice appréciable à vous (le créateur de la classe) ou à l'utilisateur, ne compliquez pas les choses en en l'ajoutant.

12.8. Exercices

Les solutions aux exercices peuvent être trouvées dans le document électronique Le Guide annoté des solutions Penser en C++, disponible à un coût modeste depuis www.BruceEckel.com.

  1. Créer une classe simple avec un opérateur surchargé operator++. Essayez d'appeler cet opérateur en forme préfixée et postfixée et voyez quel type d'avertissement vous recevez du compilateur.
  2. Créez une classe simple contenant un int et surcharger operator+ comme fonction membre. Prévoir aussi une fonction membre print( ) qui prend un ostream& comme argument et écrit dans cet ostream&. Testez votre classe pour montrer qu'elle fonctionne correctement.
  3. Ajouter un operator- binaire à l'exercice 2 comme fonction membre. Démontrez que vous pouvez utiliser vos objets dans des expressions complexes telles que a + b - c.
  4. Ajoutez un operator++ et un operator-- à l'exercice 2, à la fois en version préfixée et postfixée, de sorte qu'ils retournent l'objet incrémenté ou décrémenté. Assurez vous que les versions postfixées retournent les valeurs correctes.
  5. Modifiez les opérateurs d'incrémentation et de décrémentation dans l'exercice 4 de sorte que les versions préfixées retournent une référence non- const et les versions postfixées retournent un objetconst. Montrez qu'ils fonctionnent correctement et expliquez pourquoi il serait fait ainsi dans la pratique.
  6. Changez la fonction print( ) dans l'exercice 2 de sorte que ce soit operator<< surchargé comme dans IostreamOperatorOverloading.cpp.
  7. Modifiez l'exercice 3 de façon que operator+ et operator- soient des fonctions non membres. Démontrez qu'elles fonctionnent encore correctement.
  8. Ajoutez l'opérateur unaire operator- à l'exercice 2 et démontrez qu'il fonctionne correctement.
  9. Créez une classe qui contient un unique private char. Surchargez les opérateurs << et >> d'iostream (comme dans IostreamOperatorOverloading.cpp) et testez les. Vous pouvez les tester avec fstreams, stringstream s, et cin et cout.
  10. Déterminez la valeur constante factice que votre compilateur passe pour les opérateurs postfixés operator++ et operator--.
  11. Ecrivez une classe Number qui comporte une donnée double, et ajoutez des opérateurs surchargés pour +, -, *, /, et l'affectation. Choisissez les valeurs de retour de ces fonctions de sorte que les expressions puissent être chaînées entre elles, et pour une meilleure efficacité. Ecrivez un opérateur de conversion automatique operator double( ) .
  12. Modifiez l'exercice 11 de sorte que l'optimisation de la valeur de retour soit utilisée, si vous ne l'avez pas déjà fait.
  13. Créez une classe qui contient un pointeur, et démontrez que si vous permettez au compilateur de synthétiser l' operator= le résultat de l'utilisation de cet opérateur sera des pointeurs qui pointent la même zone mémoire. Résolvez maintenant le problème en écrivant votre propre operator= et démontrez qu'il corrige ce défaut. Assurez vous de vérifier l'auto-affectation et traitez ce cas correctement.
  14. Ecrivez une classe appelée Bird qui contient un membre string et un static int. Dans le constructeur par défaut, utilisez le int pour générer automatiquement un identificateur que vous construisez dans le string, en association avec le nom de la classe ( Bird #1, Bird #2, etc.). Ajoutez un operator<< pour ostream s pour afficher les objets Bird. Ecrivez un opérateur d'affectation operator= et un constructeur de recopie. Dans main( ), vérifiez que tout marche correctement.
  15. Ecrivez une classe nommée BirdHouse qui contient un objet, un pointeur, et une référence pour la classe Bird de l'exercice 14. Le constructeur devrait prendre les trois Bird s comme arguments. Ajoutez un operator<< pour ostream s pour BirdHouse. Interdisez l'opérateur affectation operator= et le constructeur de recopie. Dans main( ), vérifiez que tout marche correctement.
  16. Ajoutez une donnée membre int à la fois à Bird et à BirdHouse dans l'exercice 15. Ajoutez des opérateurs membres +, -, *, et / qui utilise les membres int pour effectuer les opérations sur les membres respectifs. Vérifiez que ceux ci fonctionnent.
  17. Refaire l'exercice 16 en utilisant des opérateurs non membres.
  18. Ajoutez un operator-- à SmartPointer.cpp et NestedSmartPointer.cpp.
  19. Modifiez CopyingVsInitialization.cpp de façon que tous les constructeurs affichent un message qui vous dit ce qui se passe. Vérifiez maintenant que les deux formes d'appel au constructeur de recopie (l'affectation et la forme parenthésée) sont équivalentes.
  20. Essayez de créer un opérateur non membre operator= pour une classe et voyez quel genre de messages vous recevez du compilateur.
  21. Créez une classe avec un opérateur d'affectation qui a un second argument, un string qui a une valeur par défaut qui dit “ op= call.” Créez une fonction qui affecte un objet de votre classe à un autre et montrez que votre opérateur d'affectation est appelé correctement.
  22. Dans CopyingWithPointers.cpp, enlevez l' operator= dans DogHouse et montrez que l'opérateur operator= synthétisé par le compilateur copie correctement le string mais ne fait qu'un alias du pointeur de Dog.
  23. Dans ReferenceCounting.cpp, ajoutez un static int et un int ordinaire comme données membres à Dog et à DogHouse. Dans tous les constructeurs pour les deux classes incrémentez le static int et affectez le résultat à l' int ordinaire pour garder une trace du nombre d'objets qui ont été créés. Faites les modifications nécessaires de sorte que toutes les instructions d'affichage donneront les identificateurs int des objets impliqués.
  24. Créez une classe contenant un string comme donnée membre. Initialisez le string dans le constructeur, mais ne créez pas de constructeur de recopie ni d'opérateur affectation operator=. Faites une seconde classe qui a un membre objet de votre première classe ; ne créez pas non plus de constructeur de recopie, ni d' operator= pour cette classe. Démontrez que le constructeur de recopie et l' operator= sont correctement synthétisés par le compilateur.
  25. Combinez les classes dans OverloadingUnaryOperators.cpp et Integer.cpp.
  26. Modifiez PointerToMemberOperator.cpp en ajoutant deux nouvelles fonctions membres à Dog qui ne prennent aucun argument et qui retournent void. Créez et testez un operator->* surchargé qui fonctionne avec vos deux nouvelles fonctions.
  27. Ajoutez un operator->* à NestedSmartPointer.cpp.
  28. Créez deux classes, Apple et Orange. Dans Apple, créez un constructeur qui prend un Orange comme argument. Créez une fonction qui prend un Apple et appelez cette fonction avec un Orange pour montrer que cela fonctionne. Rendez maintenant le constructeur Apple explicite pour démontrer que la conversion automatique est de la sorte empêchée. Modifiez l'appel à votre fonction de sorte que la conversion soit faite explicitement et ainsi fonctionne.
  29. Ajouter un operator* global à ReflexivityInOverloading.cpp et démontrez qu'il est réflexif.
  30. Créez deux classes et créez un operator+ et les fonctions de conversion de sorte que l'addition soit réflexive pour les deux classes.
  31. Corrigez TypeConversionFanout.cpp en créant une fonction explicite à appeler pour réaliser la conversion de type, à la place de l'un des opérateurs de conversion automatique.
  32. Ecrivez un code simple qui utilise les opérateurs +, -, *, et / pour les double s. Essayez de vous représenter comment votre compilateur génère le code assembleur et regardez l'assembleur généré pour découvrir et expliquer ce qui se passe sous le capot.

précédentsommairesuivant
Rob Murray, C++ Strategies & Tactics, Addison-Wesley, 1993, page 47.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.