MindView Inc.

[ Suggerimenti ] [ Soluzioni degli Esercizi] [ Volume 2 ] [ Newsletter Gratuita ]
[ Seminari ] [ Seminari su CD ROM ] [ Consulenza]

Pensare in C++, seconda ed. Volume 1

©2000 by Bruce Eckel

[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]

trad. italiana e adattamento a cura di Giacomo Grande

12: Sovraccaricamento degli operatori

Il sovraccaricamento degli operatori è semplicemente "uno zuccherino sintattico," che vuol dire un altro modo di chiamare le funzioni.

La differenza è che gli argomenti di questa funzione non appaiono dentro le parentesi, ma piuttosto circondano o seguono caratteri che abbiamo sempre pensato come immutabili operatori.

Ci sono due differenze tra l'uso di un operatore e una chiamata ad una funzione ordinaria. La sintassi è differente; un operatore spesso è  "chiamato" mettendolo tra gli argomenti o a volte dopo gli argomenti. La seconda differenza è che il compilatore stabilisce quale "funzione" chiamare. Per esempio, se usiamo l'operatore + con argomenti floating-point, il compilatore  "chiama" la funzione per sommare due floating-point (questa "chiamata" è tipicamente l'atto di mettere del codice in-line, o un'istruzione di un processore floating-point). Se usiamo l'operatore + con un numero floating-point e un int, il compilatore  "chiama" una funzione speciale per trasformare l' int in float, e poi  "chiama" il codice per sommare due floating-point.

Ma in  C++ è possibile definire nuovi operatori che lavorano con le classi. Questa definizione è proprio come la definizione di una funzione ordinaria, solo che il nome della funzione consiste della parola chiave operator seguita dall'operatore. Questa è la sola differenza e diventa una funzione come tutte le altre, che il compilatore chiama quando vede il tipo appropriato.

Avvertenze & rassicurazioni

Si è portati ad essere super entusiasti con il sovraccaricamento degli operatori. È un giocattolo divertente, sulle prime. Ma ricordiamoci che è solo uno zuccherino sintattico, un altro modo per chiamare una funzione. Guardando la cosa sotto questo aspetto, non abbiamo nessuna ragione di sovraccaricare un operatore, se non ci permette di rendere il codice che coinvolge la nostra classe più facile da scrivere e soprattutto più facile da leggere (notare che si legge molto più codice di quanto non se ne scriva). Se questo non è il caso, non ci dobbiamo preoccupare.

Un'altra reazione diffusa al sovraccaricamento degli operatori è il panico; improvvisamente gli operatori C non hanno più un significato familiare. "Ogni cosa è cambiata e il mio codice C farà cose diverse!" Questo non è vero. Tutti gli operatori che contengono solo dati di tipo predefinito non possono cambiare. Non potremo mai sovraccaricare operatori del tipo

1 << 4;

oppure

1.414 << 2;

Solo espressioni che contengono tipi definiti dall'utente possono avere un operatore sovraccaricato.

Sintassi

Definire un operatore sovraccaricato è come definire una funzione, ma il nome della funzione è operator@, dove @ rappresenta l'operatore da sovraccaricare. Il numero di argomenti nella lista degli argomenti dell'operatore sovraccaricato dipende da due fattori:

  1. Se è un operatore unario (un solo argomento) o binario (due argomenti).
  2. Se l'operatore è definito come funzione globale (un argomento se unario, due se binario) o come funzione membro (zero argomenti se unario, uno se binario – l'oggetto diventa l'argomento a sinistra dell'operatore).

Qui c'è una piccola classe che mostra la sintassi per il sovraccaricamento di un operatore :

//: 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;
} ///:~

I due operatori sovraccaricati sono definiti come funzioni membro inline che si annunciano quando vengono chiamate. Il singolo argomento è quello che appare alla destra dell'operatore per operatori binari. Gli operatori unari non hanno argomenti quando definiti come funzioni membro. La funzione membro viene chiamata per l'oggetto alla sinistra dell'operatore.

Per operatori non-condizionali (gli operatori condizionali restituiscono usualmente valori Boolean), quasi sempre il valore di ritorno è un oggetto o un riferimento dello stesso tipo su cui si sta operando, se i due argomenti sono dello stesso tipo (se non sono dello stesso tipo, l'interpretazione di quello che potrebbe restituire sta a noi). In questo modo possono essere costruite espressioni complicate, come:

kk += ii + jj;

L' operator+ produce un nuovo Integer (temporaneo) che viene usato come argomento rv per  operator+=. Questo dato temporaneo viene distrutto quando non è più necessario.

Operatori sovraccaricabili

Benchè sia possibile sovraccaricare quasi tutti gli operatori disponibili in C, l'uso del sovraccaricamento degli operatori è abbastanza restrittivo. In particolare, non si possono combinare operatori che non hanno nessun significato in C (come ** per rappresentare l'elevamento a potenza), non si può cambiare la precedenza degli operatori, e non si può cambiare il numero di argomenti richiesti per un operatore. Questo ha senso – tutte queste azioni produrrebbero operatori che confondono il significato, invece di chiarirlo.

Le prossime due sottosezioni mostrano esempi di tutti gli operatori  "regolari", sovraccaricati  nella forma che più verosimilmente viene usata.

Operatori unari

L'esempio seguente mostra la sintassi per sovraccaricare tutti gli operatori unari, sia nella forma di funzioni globali (non friend di funzioni membro) che di funzioni membro. Questo (esempio) va dalla classe Integer mostrata precedentemente fino ad una nuova classe byte. Il significato degli operatori specifici dipende dall'uso che ne vogliamo fare, ma teniamo presente il cliente programmatore prima di fare cose inaspettate.

Qui c'è un catalogo di tutte le funzioni unarie:

//: C12:OverloadingUnaryOperators.cpp
#include <iostream>
using namespace std;

// Funzioni non-membro:
class Integer {
  long i;
  Integer* This() { return this; }
public:
  Integer(long ll = 0) : i(ll) {}
  //Operatori senza effetti collaterali accettano argomenti 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);
  // Operatori con effetti collaterali hanno argomenti non-const&:
  // Prefisso:
  friend const Integer&
    operator++(Integer& a);
  // Postfisso:
  friend const Integer
    operator++(Integer& a, int);
  // Prefisso:
  friend const Integer&
    operator--(Integer& a);
  // Postfisso:
  friend const Integer
    operator--(Integer& a, int);
};

// Operatori globali:
const Integer& operator+(const Integer& a) {
  cout << "+Integer\n";
  return a; //L'operatore + unario non ha effetto sull'argomento
}
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 è ricorsivo!
}
int operator!(const Integer& a) {
  cout << "!Integer\n";
  return !a.i;
}
// Prefisso; restituisce il valore incrementato
const Integer& operator++(Integer& a) {
  cout << "++Integer\n";
  a.i++;
  return a;
}
// Postfisso; restituisce il valore prima di incrementarlo:
const Integer operator++(Integer& a, int) {
  cout << "Integer++\n";
  Integer before(a.i);
  a.i++;
  return before;
}
// Prefisso; restituisce il valore decrementato
const Integer& operator--(Integer& a) {
  cout << "--Integer\n";
  a.i--;
  return a;
}
// Postfisso; restituisce il valore prima di decrementarlo:
const Integer operator--(Integer& a, int) {
  cout << "Integer--\n";
  Integer before(a.i);
  a.i--;
  return before;
}

// Mostra che gli operatori sovraccaricati funzionano:
void f(Integer a) {
  +a;
  -a;
  ~a;
  Integer* ip = &a;
  !a;
  ++a;
  a++;
  --a;
  a--;
}

// Funzioni membro("this" implicito):
class Byte {
  unsigned char b;
public:
  Byte(unsigned char bb = 0) : b(bb) {}
  // Non ci sono effetti collaterali: funzione membro 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;
  }
  //Ci sono effetti collaterali: funzione membro non-const:
  const Byte& operator++() { // Prefisso
    cout << "++Byte\n";
    b++;
    return *this;
  }
  const Byte operator++(int) { // Postfisso
    cout << "Byte++\n";
    Byte before(b);
    b++;
    return before;
  }
  const Byte& operator--() { // Prefisso
    cout << "--Byte\n";
    --b;
    return *this;
  }
  const Byte operator--(int) { // Postfisso
    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);
} ///:~

Le funzioni sono raggruppate in base al modo in cui vengono passati i loro argomenti. Le linee guida su come passare e restituire argomenti saranno fornite in seguito. Le forme di sopra (e quelle che seguono nella prossima sezione) sono quelle tipicamente usate, così assumiamo queste come modello per il sovraccaricamento dei nostri operatori.

Incremento & decremento

Gli operatori sovraccaricati ++ e – – presentano un dilemma perchè vorremmo essere in grado di chiamare funzioni diverse a seconda che essi appaiano prima (prefisso) o dopo (postfisso) l'oggetto su cui operano. La soluzione è semplice, ma qualche volta la gente trova la cosa un pò fuorviante sulle prime. Quando il compilatore vede, per esempio, ++a (pre-incremento), esso genera una chiamata a operator++(a); ma quando vede a++, esso genera una chiamata a operator++(a, int). Cioè il compilatore distingue le due forme effettuando chiamate a due diverse funzioni sovraccaricate. In OverloadingUnaryOperators.cpp per le versioni riferite alle funzioni membro, se il compilatore vede ++b, genera una chiamata a B::operator++( ); se vede b++ chiama B::operator++(int).

Tutto quello che l'utente vede è che viene chiamata una funzione diversa per la versione con il prefisso e quella con il postfisso. In fondo le chiamate alle due funzioni  hanno firme diverse, così esse si aggangiano a due corpi di funzione diversi. Il compilatore passa un valore costante fittizio per l'argomento int (il quale non ha mai un identificativo, in quanto il suo valore non è mai usato) per generare una firma diversa per la versione con postfisso.

Operatori binari

Il listato seguente ripete l'esempio di OverloadingUnaryOperators.cpp per gli operatori binari, così abbiamo un esempio per tutti gli operatori che potremmo voler sovraccaricare.

//: C12:Integer.h
// Operatori sovraccaricati non-membri
#ifndef INTEGER_H
#define INTEGER_H
#include <iostream>

// Funzioni non-membro:
class Integer { 
  long i;
public:
  Integer(long ll = 0) : i(ll) {}
  // Operatori che creano valori nuovi, modificati:
  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);
  //Modifica per assegnamento & ritorno di 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);
  // Gli operatori condizionali restituiscono 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);
  // Scrive i contenuti su un ostream:
  void print(std::ostream& os) const { os << i; }
}; 
#endif // INTEGER_H ///:~
//: C12:Integer.cpp {O}
// Implementazione di operatori sovraccaricati
#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, "divide by zero");
  return Integer(left.i / right.i);
}
const Integer
  operator%(const Integer& left,
            const Integer& right) {
  require(right.i != 0, "modulo by zero");
  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);
}
// Modifica per assegnamento & ritorno di lvalue:
Integer& operator+=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-assegnamento */}
   left.i += right.i;
   return left;
}
Integer& operator-=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-assegnamento */}
   left.i -= right.i;
   return left;
}
Integer& operator*=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-assegnamento */}
   left.i *= right.i;
   return left;
}
Integer& operator/=(Integer& left,
                    const Integer& right) {
   require(right.i != 0, "divide by zero");
   if(&left == &right) {/* auto-assegnamento */}
   left.i /= right.i;
   return left;
}
Integer& operator%=(Integer& left,
                    const Integer& right) {
   require(right.i != 0, "modulo by zero");
   if(&left == &right) {/* auto-assegnamento */}
   left.i %= right.i;
   return left;
}
Integer& operator^=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-assegnamento */}
   left.i ^= right.i;
   return left;
}
Integer& operator&=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-assegnamento */}
   left.i &= right.i;
   return left;
}
Integer& operator|=(Integer& left,
                    const Integer& right) {
   if(&left == &right) {/* auto-assegnamento */}
   left.i |= right.i;
   return left;
}
Integer& operator>>=(Integer& left,
                     const Integer& right) {
   if(&left == &right) {/* auto-assegnamento */}
   left.i >>= right.i;
   return left;
}
Integer& operator<<=(Integer& left,
                     const Integer& right) {
   if(&left == &right) {/* auto-assegnamento */}
   left.i <<= right.i;
   return left;
}
// Gli operatori condizionali restituiscono 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;
} ///:~
//: C12:IntegerTest.cpp
//{L} Integer
#include "Integer.h"
#include <fstream>
using namespace std;
ofstream out("IntegerTest.out");

void h(Integer& c1, Integer& c2) {
  // Un'espressione complessa:
  c1 += c1 * c2 + c2 % c1;
  #define TRY(OP) \
    out << "c1 = "; c1.print(out); \
    out << ", c2 = "; c2.print(out); \
    out << ";  c1 " #OP " c2 produces "; \
    (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(<<=)
  // Espressioni condizionali:
  #define TRYC(OP) \
    out << "c1 = "; c1.print(out); \
    out << ", c2 = "; c2.print(out); \
    out << ";  c1 " #OP " c2 produces "; \
    out << (c1 OP c2); \
    out << endl;
  TRYC(<) TRYC(>) TRYC(==) TRYC(!=) TRYC(<=)
  TRYC(>=) TRYC(&&) TRYC(||)
} 

int main() {
  cout << "friend functions" << endl;
  Integer c1(47), c2(9);
  h(c1, c2);
} ///:~
//: C12:Byte.h
// Operatori sovraccaricati membri di classi
#ifndef BYTE_H
#define BYTE_H
#include "../require.h"
#include <iostream>
// Funzioni membro ("this" implicito):
class Byte { 
  unsigned char b;
public:
  Byte(unsigned char bb = 0) : b(bb) {}
  // Senza effetti collaterali: funzioni membro 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, "divide by zero");
    return Byte(b / right.b);
  }
  const Byte
    operator%(const Byte& right) const {
    require(right.b != 0, "modulo by zero");
    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);
  }
  // Modifica per assegnamento & ritorno di lvalue.
  // operator= può essere solo funzione membro:
  Byte& operator=(const Byte& right) {
    // Gestisce l'auto-assegnamento:
    if(this == &right) return *this;
    b = right.b;
    return *this;
  }
  Byte& operator+=(const Byte& right) {
    if(this == &right) {/* auto-assegnamento */}
    b += right.b;
    return *this;
  }
  Byte& operator-=(const Byte& right) {
    if(this == &right) {/* auto-assegnamento */}
    b -= right.b;
    return *this;
  }
  Byte& operator*=(const Byte& right) {
    if(this == &right) {/* auto-assegnamento */}
    b *= right.b;
    return *this;
  }
  Byte& operator/=(const Byte& right) {
    require(right.b != 0, "divide by zero");
    if(this == &right) {/* auto-assegnamento */}
    b /= right.b;
    return *this;
  }
  Byte& operator%=(const Byte& right) {
    require(right.b != 0, "modulo by zero");
    if(this == &right) {/* auto-assegnamento */}
    b %= right.b;
    return *this;
  }
  Byte& operator^=(const Byte& right) {
    if(this == &right) {/* auto-assegnamento */}
    b ^= right.b;
    return *this;
  }
  Byte& operator&=(const Byte& right) {
    if(this == &right) {/* auto-assegnamento */}
    b &= right.b;
    return *this;
  }
  Byte& operator|=(const Byte& right) {
    if(this == &right) {/* auto-assegnamento */}
    b |= right.b;
    return *this;
  }
  Byte& operator>>=(const Byte& right) {
    if(this == &right) {/* auto-assegnamento */}
    b >>= right.b;
    return *this;
  }
  Byte& operator<<=(const Byte& right) {
    if(this == &right) {/* auto-assegnamento */}
    b <<= right.b;
    return *this;
  }
  // Operatori condizionali restituiscono 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;
  }
  // Scrive i contenuti su un ostream:
  void print(std::ostream& os) const {
    os << "0x" << std::hex << int(b) << std::dec;
  }
}; 
#endif // BYTE_H ///:~
//: 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 produces "; \
    (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(=) // Operatori di assegnamento

  // Espressioni condizionali:
  #define TRYC2(OP) \
    out << "b1 = "; b1.print(out); \
    out << ", b2 = "; b2.print(out); \
    out << ";  b1 " #OP " b2 produces "; \
    out << (b1 OP b2); \
    out << endl;

  b1 = 9; b2 = 47;
  TRYC2(<) TRYC2(>) TRYC2(==) TRYC2(!=) TRYC2(<=)
  TRYC2(>=) TRYC2(&&) TRYC2(||)

  // Assegnamenti concatenati:
  Byte b3 = 92;
  b1 = b2 = b3;
}

int main() {
  out << "member functions:" << endl;
  Byte b1(47), b2(9);
  k(b1, b2);
} ///:~

Si può vedere che all' operator= è consentito essere solo una funzione membro. Questo è spiegato in seguito.

Notare che tutti gli operatori di assegnamento hanno del codice per controllare l'auto-assegnamento; questo è una linea guida generale. In certi casi questo non è necessario; per esempio, con operator+= possiamo scrivere A+=A e ottenere la somma di A con se stesso. Dove è più importante controllare l'auto assegnamento è nell'operator= perchè con oggetti complicati si possono ottenere risultati disastrosi (in certi casi è OK, ma bisogna sempre stare attenti quando si scrive  operator=).

Tutti gli operatori mostrati nei due esempi precedenti sono sovraccaricati per manipolare un singolo tipo. È anche possibile sovraccaricare operatori per manipolare tipi misti e sommare, per esempio, mele con arancie. Prima di cominciare ad usare in modo esaustivo il sovraccaricamento degli operatori, tuttavia, bisogna dare uno sguardo alla sezione sulla conversione automatica dei tipi più avanti in questo capitolo. Spesso una conversione di tipo alla destra dell'operatore può far risparmiare un sacco di operatori sovraccaricati.

Argomenti & valori di ritorno

Può sembrare leggermente fuorviante sulle prime quando si guarda dentro OverloadingUnaryOperators.cpp, Integer.h e Byte.h e si vedono tutti i modi diversi in cui vengono passati gli argomenti e restituiti i valori di ritorno. Benchè si possano passare e restituire gli argomenti in tutti i modi possibili, le scelte fatte in questi esempi non sono casuali. Esse seguono un modello logico, che è lo stesso che dovremmo usare nella maggior parte dei casi.

  1. Come per una qualunque argomento di una funzione, se l'argomento serve solo in lettura e non bisogna fare cambiamenti su di esso è bene passarlo come riferimento const. Le operazioni aritmetiche ordinarie (come + e , ecc.) e i Booleani non cambiano i loro argomenti, e quindi il modo più diffuso di passare gli argomenti è come riferimento const. Quando la funzione è un membro di una classe, questo si traduce nell'avere funzioni membro const. Solo con gli operatori di assegnamento (come +=) e l' operator=, che cambiano l'argomento alla sinistra dell'operatore, l'argomento di sinistra non è una costante, ma viene passato come indirizzo perchè viene modificato.

  2. Il tipo del valore di ritorno che dobbiamo scegliere dipende dal significato atteso dell'operatore (di nuovo, si può fare qualunque cosa con gli argomenti e i valori di ritorno.) Se l'effetto dell'operatore è di produrre un nuovo valore, c'è bisogno di generare un nuovo oggetto come valore di ritorno. Per esempio, Integer::operator+ deve produrre un oggetto Integer che è la somma degli operandi. Questo oggetto viene restituito per valore come const, così il risultato non può essere modificato come un lvalue (valore di sinistra).

  3. Tutti gli operatori di assegnamento modificano il valore di sinistra. Per consentire al risultato di un assegnamento di essere usato in espressioni concatenate, come a=b=c, ci si aspetta che il valore di ritorno sia un riferimento allo stesso valore di sinistra appena modificato. Ma questo riferimento deve essere const o nonconst? Benchè noi leggiamo l'espressione a=b=c da sinistra verso destra, il compilatore la analizza da destra verso sinistra, cosicchè non siamo obbligati a restituire un nonconst per supportare il concatenamento dell'assegnazione. Tuttavia la gente qualche volta si aspetta di poter effettuare operazioni sull'oggetto a cui è stata appena fatta l'assegnazione, come (a=b).func( ); per chiamare func( ) su a dopo avergli assegnato b. Perciò il valore di ritorno per tutti gli operatori di assegnamento potrebbe essere un riferimento nonconst al valore di sinistra.

  4. Per gli operatori logici tutti si aspettano di avere come valore di ritorno nel peggiore dei casi un int e nel migliore dei casi un bool (le librerie sviluppate prima che molti compilatori C++ supportassero il tipo incorporato bool, usano un int o un typedef equivalente).

Gli operatori di incremento e decremento presentano il solito dilemma delle versioni pre- e postfissa. Entrambe le versioni modificano l'oggetto e quindi non possono trattare l'oggetto come  const. La versione con prefisso restituisce il valore dell'oggetto dopo averlo modificato, quindi ci si aspetta come ritorno lo stesso oggetto su cui opera. In questo caso, quindi, si può semplicemente restituire un *this come riferimento. La versione con postfisso si suppone che restituisca il valore dell'oggetto prima che questo venga modificato, perciò siamo obbligati a creare un oggetto separato per rappresentare questo valore e restituirlo. In questo caso il ritorno deve essere fatto per valore se si vuole preservare il significato atteso (Notare che qualche volta si possono trovare gli operatori di incremento e decremento che restituiscono un  int o un bool per indicare, per esempio, che un oggetto utilizzato per scorrere una lista ha raggiunto la fine di questa lista). Adesso la domanda è: il valore di ritorno deve essere const o nonconst? Se si consente all'oggetto di essere modificato e qualcuno scrive (++a).func( ), func( ) opera su a stessa, ma con (a++).func( ), func( ) opera su un oggetto temporaneo restituito da operator++ in versione con postfisso. Gli oggetti temporanei sono automaticamente const, perciò questo sarà marcato dal compilatore, ma per coerenza ha molto più senso renderli entrambi const, come viene fatto qui. Oppure si può scegliere di rendere  non-const la versione con prefisso e const la versione con postfisso. A causa della varietà di significati che si possono dare agli operatori di incremento e decremento, è necessario considerarli caso per caso.

Ritorno per valore come const

Il ritorno per valore come  const potrebbe sembrare un pò subdolo sulle prime, perciò richiede un minimo di spiegazione. Consideriamo l'operatore binario operator+. Se lo usiamo in un'espressione come f(a+b), il risultato di a+b diventa un oggetto temporaneo usato nella chiamata di f( ). Siccome è temporaneo, esso è automaticamente const, perciò non ha nessun effetto la dichiarazione esplicita const o non-const.

Tuttavia è anche possibile inviare un messaggio al valore di ritorno di a+b, piuttosto che passarlo semplicemente ad una funzione. Per esempio possiamo scrivere (a+b).g( ), in cui g( ) è una qualche funzione membro della classe Integer, in questo caso. Rendendo const  il valore di ritorno si impone che solo le funzioni membro const possano essere chiamate per questo valore di ritorno. Questo è un uso corretto di const, perchè previene il potenziale errore di memorizzare un'informazione preziosa in un oggetto che molto probabilmente verrebbe perso.

Ottimizzazione del valore di ritorno

Quando vengono creati nuovi oggetti da restituire per valore, guardiamo la forma usata. In operator+, per esempio:

return Integer(left.i + right.i);

Questa potrebbe sembrare sulle prime una "chiamata a funzione di un costruttore," ma non lo è. La sintassi è quella di un oggetto temporaneo; l'istruzione dice "costruisci un oggetto temporaneo di tipo Integer e restituiscilo." Per questo, si potrebbe pensare che il risultato è lo stesso che si ottiene creando un oggetto locale e restituendolo. Invece la cosa è alquanto diversa. Se invece scriviamo:

Integer tmp(left.i + right.i);
return tmp;

succedono tre cose. Primo, l'oggetto tmp viene creato includendo la chiamata al suo costruttore. Secondo, il costruttore di copia copia  tmp nella locazione del valore di ritorno (esterna allo scope). Terzo, alla fine viene chiamato il distruttore di tmp.

In contrasto, l'approccio del  "ritorno di un oggetto temporaneo" funziona in maniera completamente diversa. Quando il compilatore vede fare una cosa del genere, sa che non c'è nessun altro interesse sull'oggetto creato se non quello di usarlo come valore di ritorno. Il compilatore trae vantaggio da questa informazione, creando l'oggetto direttamente dentro la locazione del valore di ritorno, esterna allo scope. Questo richiede solo una chiamata al costruttore ordinario (non è necessario alcun costruttore di copia) e non c'è la chiamata al distruttore, in quanto di fatto non viene mai creato l'oggetto temporaneo. Così, se da una parte non costa nulla se non un pò di accortezza da parte del programmatore, dall'altra è significativamente molto più efficiente. Questo viene spesso chiamata ottimizzazione del valore di ritorno.

Operatori inusuali

Alcuni operatori aggiuntivi hanno una sintassi leggermente diversa per il sovraccaricamento.

L'operatore sottoscrizione, operator[ ], deve essere una funzione membro e richiede un singolo argomento. Siccome l' operator[ ] implica che l'oggetto per il quale viene chiamato agisce come un array, il valore di ritorno per questo operatore deve essere un riferimento, così può essere usato convenientemente alla sinistra del segno di uguale. Questo operatore viene comunemente sovraccaricato; se ne potranno vedere esempi nel resto del libro.

Gli operatori new e delete controllano l'allocazione dinamica della memoria e possono essere sovraccaricati in molti modi diversi. Questo argomento è trattato nel Capitolo 13.

Operatore virgola

L'operatore virgola viene chiamato quando appare dopo un oggetto del tipo per cui l'operatore virgola è definito . Tuttavia, "operator," (operatore virgola)  non viene chiamato per le liste di argomenti di funzioni, ma solo per oggetti che appaiono separati da virgole. Non sembra che ci siano tanti usi pratici di questo operatore ed è stato messo nel linguaggio solo per coerenza. Qui c'è un esempio di come la funzione virgola può essere chiamata quando appare una virgola prima o dopo  un oggetto:

//: 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;  // Viene chiamato l'operatore virgola

  Before c;
  1, c;  // Viene chiamato l'operatore virgola
} ///:~

La funzione globale consente alla virgola di essere posta prima dell'oggetto in questione. L'uso mostrato è piuttosto oscuro e opinabile. Benchè si potrebbe usare un lista separata da virgole come parte di un'espressione più complessa, è molto subdolo nella maggior parte delle situazioni.

Operator->

L' operator–> (dereferenziazione di puntatore) è generalmente usato quando si vuole fare apparire un oggetto come puntatore. Siccome un oggetto di questo tipo incorpora molta più "intelligenza" di quanta ne abbia un puntatore normale, spesso viene chiamato puntatore intelligente. Questo è utile soprattutto se si vuole "avvolgere" una classe intorno ad un puntatore per renderlo sicuro, o nell'uso comune di un iteratore, cioè un oggetto che scorre all'interno di una collezione /contenitore di altri oggetti e li seleziona uno alla volta, senza fornire l'accesso diretto all'implementazione del contenitore (I contenitori e gli iteratori si possono trovare spesso nelle librerie di classi, come nella libreria Standard del C++, descritta nel Volume 2 di questo libro).

Questo operatore deve essere una funzione membro. Esso ha dei vincoli aggiuntivi atipici: deve restituire un oggetto (o un riferimento ad un oggetto) che abbia anch'esso un operatore di dereferenziazione di puntatore, oppure deve restituire un puntatore che può essere usato per selezionare quello a cui punta l'operatore. Qui c'è un esempio:

//: 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; }
};

// Definizioni di membri static:
int Obj::i = 47;
int Obj::j = 11;

// Container:
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;
  }
  // Il valore di ritorno indica la fine della lista:
  bool operator++() { // Prefisso
    if(index >= oc.a.size()) return false;
    if(oc.a[++index] == 0) return false;
    return true;
  }
  bool operator++(int) { // Postfisso
    return operator++(); // Usa la versione con prefisso
  }
  Obj* operator->() const {
    require(oc.a[index] != 0, "Zero value "
      "returned by 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]); // Lo riempie
  SmartPointer sp(oc); // Crea un iteratore
  do {
    sp->f(); // Chiamata all'operatore di dereferenziazione di puntatore
    sp->g();
  } while(sp++);
} ///:~

La classe Obj definisce gli oggetti che vengono manipolati in questo programma. Le funzioni f( ) e g( ) semplicemente stampano valori di interesse usando i dati membri static. I puntatori a questi oggetti sono memorizzati all'interno dei contenitori di tipo ObjContainer usando la sua funzione add( ). ObjContainer si presenta come un array di puntatori, ma si può notare che non c'è nessun modo di tirar fuori questi puntatori. Tuttavia, SmartPointer è dichiarata come classe friend, perciò ha il permesso di guardare dentro al contenitore. La classe SmartPointer si presenta molto più come un puntatore intelligente – lo si può spostare in avanti usando l' operator++ (si può anche definire un operator– –), non può oltrepassare la fine del contenitore a cui punta, e produce (attraverso l'operatore di dereferenziazione di puntatori) il valore a cui punta. Notare che SmartPointer è un adattamento personalizzato per il contenitore per cui è stato creato; a differenza di un puntatore ordinario, non c'è un puntatore intelligente "general purpose". Si può apprendere molto di più sui puntatori intelligenti detti "iteratori" nell'ultimo capitolo di questo libro e nel Volume 2  (scaricabile da www.BruceEckel.com).

In main( ), una volta che il contenitore oc è riempito con oggetti di tipo Obj , viene creato uno SmartPointer sp. Le chiamate al puntatore intelligente avvengono nelle espressioni:

sp->f(); // Chiamate a puntatori intelligenti
sp->g(); 

Qui, anche se sp di fatto non ha le funzioni membro f( ) e g( ), l'operatore di dereferenziazione di puntatori automaticamente chiama queste funzioni per l'Obj* restituito da SmartPointer::operator–>. Il compilatore effettua tutti i controlli per assicurare che la chiamata alla funzione lavori correttamente.

Un iteratore nidificato

È molto più comune vedere una classe "puntatore intelligente" o "iteratore" nidificata all'interno della classe che essa serve. L'esempio precedente può essere riscritto per nidificare SmartPointer all'interno di ObjContainer, come segue:

//: 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; }
};

// Definizioni di membri Static:
int Obj::i = 47;
int Obj::j = 11;

// Container:
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;
    }
    // Il valore di ritorno indica la fine della lista:
    bool operator++() { // Prefisso
      if(index >= oc.a.size()) return false;
      if(oc.a[++index] == 0) return false;
      return true;
    }
    bool operator++(int) { // Postfisso
      return operator++(); // Usa la versione con prefisso
    }
    Obj* operator->() const {
      require(oc.a[index] != 0, "Zero value "
        "returned by SmartPointer::operator->()");
      return oc.a[index];
    }
  };
  // Funzione per produrre un puntatore intelligente che 
  // punta all'inizio di 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]); //Lo riempie
  ObjContainer::SmartPointer sp = oc.begin();
  do {
    sp->f(); // Chiamata all'operatore di dereferenziazione di puntatore
    sp->g();
  } while(++sp);
} ///:~

Oltre alla nidificazione delle classi, ci sono solo due differenze qui. La prima è nella dichiarazione della classe in modo che possa essere friend:

class SmartPointer;
friend SmartPointer;

Il compilatore deve innanzitutto sapere che la classe esiste prima che possa dire che è una friend.

La seconda differenza è nella funzione begin( ), membro  di ObjContainer, che produce uno  SmartPointer che punta all'inizio della sequenza ObjContainer. Benchè sia solo una questione di convenienza, è preziosa perchè segue parte della forma usata nella libreria Standard del C++.

Operator->*

L' operator–>* è un operatore binario che si comporta come tutti gli altri operatori binari. Viene fornito per quelle situazioni in cui si vuole simulare il comportamento di un  puntatore-a-membro, descritto nel capitolo precedente.

Proprio come l' operator->, l'operatore di dereferenziazione di puntatore-a-membro viene generalmente usato con certi tipi di oggetti che rappresentano un "puntatore intelligente," anche se l'esempio mostrato qui è più semplice, in modo da risultare comprensibile. Il trucco quando si definisce l' operator->* è che deve ritornare un oggetto per il quale l' operator( ) (chiamata a funzione) può essere chiamato con gli argomenti da passare  alla funzione membro che si sta chiamando.

L'operatore chiamata a funzione,  operator( ), deve essere una funzione membro, ed è unica per il fatto che accetta un numero qualsiasi di argomenti. Esso fa si che il nostro oggetto si presenti come se fosse una funzione. Anche se è possibile definire diverse funzioni con l' operator( ) sovraccaricato, con diversi argomenti, questo viene spesso usato per tipi che hanno una sola operazione (funzione membro) o che hanno almeno un'operazione particolarmente importante rispetto alle altre. Possiamo vedere nel Volume 2 che la Libreria dello Standard C++ usa l'operatore chiamata a funzione per creare "oggetti funzione."

Per creare un operator->* bisogna prima creare una classe con un operator( ) che rappresenta il tipo di oggetto che l' operator->* deve restituire. Questa classe deve in qualche modo catturare le informazioni necessarie affinchè quando l' operator( ) viene chiamato (il che avviene automaticamente), il puntatore-a-membro viene dereferenziato per l'oggetto. Nell'esempio seguente, il costruttore FunctionObject cattura e memorizza sia il puntatore all'oggetto che il puntatore alla funzione membro, e quindi l' operator( ) li usa per costruire la chiamata al puntatore-a-membro:

//: C12:PointerToMemberOperator.cpp
#include <iostream>
using namespace std;

class Dog {
public:
  int run(int i) const { 
    cout << "run\n";  
    return i; 
  }
  int eat(int i) const { 
     cout << "eat\n";  
     return i; 
  }
  int sleep(int i) const { 
    cout << "ZZZ\n"; 
    return i; 
  }
  typedef int (Dog::*PMF)(int) const;
  // operator->* deve restituire un oggetto 
  // che ha un operator():
  class FunctionObject {
    Dog* ptr;
    PMF pmem;
  public:
    // Salva il puntatore all'oggetto e il puntatore al membro
    FunctionObject(Dog* wp, PMF pmf) 
      : ptr(wp), pmem(pmf) { 
      cout << "FunctionObject constructor\n";
    }
    // Effettua la chiamata usando il puntatore all'oggetto
    // e il puntatore al membro
    int operator()(int i) const {
      cout << "FunctionObject::operator()\n";
      return (ptr->*pmem)(i); // Effettua la chiamata
    }
  };
  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 ha tre funzioni membro, e tutte prendono un argomento int e restituiscono un int. PMF è un typedef per semplificare la definizione di un puntatore-a-membro per le funzioni membro di Dog.

Con operator->* viene creata e restituita una FunctionObject. Notare che operator->* conosce sia il puntatore-a-membro che l'oggetto per cui questo viene chiamato (this), e li passa al costruttore di FunctionObject che ne memorizza i valori. Quando viene chiamato operator->*, il compilatore chiama immediatamente l' operator( ) per calcolare il valore di ritorno di operator->*, passandogli gli argomenti di operator->*. L'operatore FunctionObject::operator( ) prende gli argomenti e dereferenzia il  "reale" puntatore-a-membro usando il puntatore all'oggetto e il puntatore-a-membro da esso memorizzati.

Notare che quello che stiamo facendo qui, proprio come con l' operator->, è di inserirci nel mezzo della chiamata ad operator->*. Questo ci permette di fare delle operazioni extra, se ne abbiamo bisogno.

Il meccanismo dell'operator->* implementato qui funziona solo con funzioni membro che prendono come argomento un int e restituiscono un int. Questo è limitativo, ma creare un meccanismo di sovraccaricamento per ogni singola possibilità sembra un compito proibitivo. Fortunatamente il meccanismo dei template del C++ (descritto nell'ultimo capitolo di questo libro e nel Volume 2) è stato pensato proprio per gestire problemi di questo tipo.

Operatori che non si possono sovraccaricare

Ci sono certi operatori tra quelli disponibili che non sono sovraccaricabili. Il motivo generico per questa restrizione è la sicurezza. Se questi operatori fossero sovraccaricabili, si potrebbero in qualche modo mettere a repentaglio o rompere i meccanismi di sicurezza, rendendo le cose difficili o confondendo la pratica esistente.

Operatori non-membro

In alcuni degli esempi precedenti, gli operatori possono essere membri o non-membri e non sembra esserci molta differenza. Questo in genere solleva una questione, "Quale dobbiamo scegliere?" In generale, se non c'è differenza è preferibile scegliere un membro di classe, per enfatizzare l'associazione tra l'operatore e la sua classe. Quando l'operando di sinistra è sempre un oggetto della classe corrente questo funziona benissimo.

Tuttavia qualche volta si vuole che l'operando di sinistra sia un oggetto di qualche altra classe. Un caso comune in cui si può vedere questo è quando gli operatori << e >> sono sovraccaricati per iostreams. Siccome iostreams è una libreria fondamentale del C++, probabilmente si vogliono sovraccaricare questi operatori per molte classi proprie, per cui il problema merita di essere affrontato:

//: C12:IostreamOperatorOverloading.cpp
// Esempi di operatori sovraccaricati non-membri
#include "../require.h"
#include <iostream>
#include <sstream> // "String streams"
#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[] out of range");
    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; // Usa l'operatore sovraccaricato operator[]
  cout << I;
} ///:~

Questa classe contiene anche un operator [ ] sovraccaricato, che restituisce un riferimento al giusto valore nell'array. Siccome viene restituito un riferimento, l'espressione

I[4] = -1;

non solo si presenta in modo molto più umano di quanto non si sarebbe ottenuto con l'uso dei puntatori, ma sortisce anche l'effetto desiderato.

È importante che gli operatori di shift sovraccaricati ritornino  per riferimento, in modo che l'azione si rifletta sugli oggetti esterni. Nelle definizioni di funzioni, un'espressione come

os << ia.i[j];

causa la chiamata alle funzioni esistenti dell'operatore sovraccaricato (cioè quelle definite in <iostream>). In questo caso, la funzione chiamata è ostream& operator<<(ostream&, int) perchè ia.i[j] si risolve in un int.

Una volta che sono state effettuate tutte le azioni su istream o su ostream, questo viene restituito in modo che possa essere usato in espressioni più complicate.

In main( ) viene usato un nuovo tipo di iostream:  stringstream (dichiarato in <sstream>). Questa è una classe che prende una string (che può costruire a partire da un array di char, come mostrato qui) e la traduce in un iostream. Nell'esempio di sopra, questo significa che gli operatori di shift possono essere testati senza aprire un file o digitando dati dalla linea di comando.

La forma mostrata in questo esempio per l'inserimento e l'estrazione dei dati è standard. Se si vogliono creare questi operatori per la propria classe si può copiare il prototipo delle funzioni e i tipi di ritorno e seguire la stessa forma per il corpo.

Linee guida di base

Murray[49]  suggerisce queste linee guida per la scelta tra membri e non-membri:

Operatore

Uso raccomandato

Tutti gli operatori unari

membri

= ( ) [ ] –> –>*

Devono essere membri

+= –= /= *= ^=
&= |= %= >>= <<=

membri

Tutti gli altri operatori binari

non-membri

Assegnamento con il sovraccaricamento

Una fonte comune di confusione per i nuovi programmatori in C++ è l'assegnamento. Questo perchè il segno di = (uguale) è un'operazione fondamentale nella programmazione, dal livello più alto fino alla copia di un registro a livello macchina. In più qualche volta viene invocato anche il costruttore di copia (descritto nel Capitolo 11) quando si usa il segno di =:

MyType b;
MyType a = b;
a = b;

Nella seconda linea viene definito l'oggetto a. Un nuovo oggetto viene creato laddove non ne esisteva uno prima. Siccome sappiamo come il compilatore C++ sia difensivo riguardo all'inizializzazione di oggetti, sappiamo anche che deve essere sempre chiamato un costruttore dove si definisce un oggetto. Ma quale costruttore? a viene creato da un oggetto di tipo MyType preesistente (b, sul lato destro del segno di =), per cui c'è una sola scelta: il costruttore di copia. Anche se è coinvolto un segno di uguale, viene di fatto chiamato il costruttore di copia.

Nella terza linea le cose sono diverse. Sul lato sinistro del segno di uguale c'è un oggetto precedentemente inizializzato. Chiaramente non viene chiamato un costruttore per un oggetto che è stato già creato. In questo caso viene chiamato l'operatore MyType::operator= per a, prendendo come argomento tutto ciò che si trova sul lato destro del segno di uguale (si possono avere più funzioni operator= per trattare differenti titpi di argomenti ).

Questo comportamento non è limitato al costruttore di copia. Ogni volta che si inizializza un oggetto usando un  segno di = invece della forma ordinaria di chiamata a funzione del costruttore, il compilatore cerca un costruttore che accetta tutto ciò che sta sul lato destro dell'operatore:

//: 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)
} ///:~

Quando si ha a che fare con il segno di = è importante avere in mente questa distinzione : se l'oggetto non è stato ancora creato, è richiesta un'inizializzazione; altrimenti viene usato l' operator=.

È comunque meglio evitare di scrivere codice che usa il segno di  = per l'inizializzazione; usare, invece, sempre la forma esplicita del costruttore. Le due costruzioni con il segno di uguale diventano quindi:

Fee fee(1);
Fee fum(fi);

In questo modo si evita di confondere il lettore.

Il comportamento dell'operator=

In Integer.h e Byte.h, asseriamo che operator= può essere solo una funzione membro. Esso è intimamente legato all'oggetto sul lato sinistro di ‘='. Se fosse possibile definire l' operator= globalmente, potremmo tentare di ridefinire il segno ‘=' predefinito:

int operator=(int, MyType); // Global = non consentito!

Il compilatore evita questa possibilità forzando l' operator= ad essere una funzione membro.

Quando si crea un operator=, bisogna copiare tutte le informazioni necessarie dall'oggetto presente sul lato destro all'oggetto corrente (cioè l'oggetto per il quale l' operator= viene chiamato) per effettuare quello che consideriamo un  "assegnamento" per la nostra classe. Per oggetti semplici questo è ovvio:

//: C12:SimpleAssignment.cpp
// Semplice 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 after assignment: " << a << endl;
} ///:~

Qui l'oggetto sul lato sinistro del segno di = copia tutti gli elementi dell'oggetto sulla destra, e poi ritorna un riferimento a se stesso, il che permette di creare un'espressione molto più complessa.

Questo esempio contiene un errore comune. Quando si effettua un assegnamento tra due oggetti dello stesso tipo bisogna sempre fare il controllo sull'auto assegnamento: l'oggetto viene assegnato a se stesso? In certi casi, come questo, è innocuo effettuare l'operazione di assegnamento in qualunque modo, ma se vengono apportate modifiche all'implementazione della classe ci possono essere delle differenze, e se non si tiene conto di questo si possono introdurre errori molto difficili da scovare.

I puntatori nelle classi

Cosa succede se l'oggetto non è così semplice? Per esempio, cosa succede se l'oggetto contiene puntatori ad altri oggetti? Semplicemente, copiando un puntatore si finisce per avere due oggetti che puntano alla stessa locazione di memoria. In situazioni come queste, bisogna fare un pò di conti.

Ci sono due approcci comuni al problema. La tecnica più semplice è quella di copiare tutti i dati puntati dal puntatore quando si fa l'assegnamento o si invoca il costruttore di copia. Questo è chiaro:

//: C12:CopyingWithPointers.cpp
// Soluzione del problema della replica del puntatore
// con la duplicazione dell'oggetto che viene puntato 
// durante l'assegnamento e la costruzione della copia.
#include "../require.h"
#include <string>
#include <iostream>
using namespace std;

class Dog {
  string nm;
public:
  Dog(const string& name) : nm(name) {
    cout << "Creating Dog: " << *this << endl;
  }
  // Il costruttore di copia & operator= 
  // sono corretti.
  // Crea un oggetto Dog da un puntatore a Dog:
  Dog(const Dog* dp, const string& msg) 
    : nm(dp->nm + msg) {
    cout << "Copied dog " << *this << " from "
         << *dp << endl;
  }
  ~Dog() { 
    cout << "Deleting Dog: " << *this << endl;
  }
  void rename(const string& newName) {
    nm = newName;
    cout << "Dog renamed to: " << *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, " copy-constructed")),
      houseName(dh.houseName 
        + " copy-constructed") {}
  DogHouse& operator=(const DogHouse& dh) {
    // Controlla l'auto-assegnamento:
    if(&dh != this) {
      p = new Dog(dh.p, " assigned");
      houseName = dh.houseName + " assigned";
    }
    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 
      << "] contains " << *dh.p;
  }
}; 

int main() {
  DogHouse fidos(new Dog("Fido"), "FidoHouse");
  cout << fidos << endl;
  DogHouse fidos2 = fidos; // Costruzione della copia
  cout << fidos2 << endl;
  fidos2.getDog()->rename("Spot");
  fidos2.renameHouse("SpotHouse");
  cout << fidos2 << endl;
  fidos = fidos2; // Assegnamento
  cout << fidos << endl;
  fidos.getDog()->rename("Max");
  fidos2.renameHouse("MaxHouse");
} ///:~

Dog è una classe semplice che contiene solo una string che memorizza il nome del cane. Tuttavia bisogna in genere sapere quando succede qualcosa a Dog perchè i costruttori e i distruttori  stampano informazioni quando vengono chiamati. Notare che  il secondo costruttore è come un costruttore di copia, solo che prende un puntatore a Dog invece di un riferimento, ed ha un secondo argomento che è un messaggio che viene concatenato al nome dell'argomento Dog. Questo viene usato per aiutare nel tracciamento del comportamento del programma.

Si può vedere che ogni volta che una funzione membro stampa delle informazioni, non accede direttamente a queste informazioni, ma invia *this a cout. Questo a sua volta chiama l' ostream operator<<. È importante farlo in questo modo, perchè se vogliamo riformattare il modo in cui le informazioni di Dog vengono visualizzate (come abbiamo fatto aggiungendo ‘[' e ‘]') bisogna agire in un posto solo.

Un DogHouse contiene un Dog* e mostra le quattro funzioni che bisogna sempre definire quando la classe contiene dei puntatori: tutti i costruttori ordinari necessari, il costruttore di copia, operator= (o lo si definisce o se ne impedisce l'uso), e il distruttore. L' operator= fa il controlo sull'auto-assegnamento, come deve essere, anche se qui non è strettamente necessario. Questo elimina virtualmente la possibilità di dimenticarsene se si fanno delle modifiche al codice che lo rendono necessario.

Il conteggio dei Riferimenti

Nell'esempio di sopra, il costruttore di copia e l' operator= effettuano una nuova copia dei dati puntati dal puntatore, e il distruttore la cancella. Tuttavia se il nostro oggetto richiede un sacco di memoria o un elevato overhead di inizializzazione, si vorrebbe evitare questa copia. Un approccio comune a questo problema è chiamato conteggio dei riferimenti. Si conferisce intelligenza all'oggetto che viene puntato in modo che esso sa quanti oggetti lo stanno puntando. La costruzione-copia o l'assegnamento comportano l'aggiunta di un altro puntatore ad un oggetto esistente e l'incremento di un contatore di riferimenti. La distruzione comporta il decremento del contatore dei riferimenti e la distruzione dell'oggetto se il contatore arriva a zero.

Ma cosa succede se vogliamo scrivere nell'oggetto ( Dog nell'esempio di sopra)? Più di un oggetto può usare questo Dog, cosicchè potremmo andare a modificare il Dog di qualcun altro come il nostro, il che non sembra essere molto carino. Per risolvere questo problema di "duplicazione" viene usata una tecnica aggiuntiva chiamata copia-su-scrittura. Prima di scrivere un blocco di memoria ci si assicura che nessun altro vi sta scrivendo. Se il contatore di riferimenti è maggiore di uno bisogna fare una copia personale del blocco di memoria prima di scriverci dentro, in modo da non disturbare il territorio di qualcun altro . Qui c'è un semplice esempio del contatore di riferimenti e della copia su scrittura:

//: C12:ReferenceCounting.cpp
// Contatore di riferimenti, copia-su-scrittura
#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 << "Creating Dog: " << *this << endl;
  }
  // Previene l'assegnamento:
  Dog& operator=(const Dog& rv);
public:
  // Gli oggetti Dog possono essere creati solo sull'heap:
  static Dog* make(const string& name) {
    return new Dog(name);
  }
  Dog(const Dog& d) 
    : nm(d.nm + " copy"), refcount(1) {
    cout << "Dog copy-constructor: " 
         << *this << endl;
  }
  ~Dog() { 
    cout << "Deleting Dog: " << *this << endl;
  }
  void attach() { 
    ++refcount;
    cout << "Attached Dog: " << *this << endl;
  }
  void detach() {
    require(refcount != 0);
    cout << "Detaching Dog: " << *this << endl;
    // Distrugge l'oggetto se nessuno lo sta usando:
    if(--refcount == 0) delete this;
  }
  // Copia questo oggetto Dog in modo condizionale.
  // La chiama prima di modificare Dog, assegna
  // il puntatore di ritorno a Dog*.
  Dog* unalias() {
    cout << "Unaliasing Dog: " << *this << endl;
    // Non lo duplica se non replicato:
    if(refcount == 1) return this;
    --refcount;
    // Usa il costruttore di copia per duplicare:
    return new Dog(*this);
  }
  void rename(const string& newName) {
    nm = newName;
    cout << "Dog renamed to: " << *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 << "Created DogHouse: "<< *this << endl;
  }
  DogHouse(const DogHouse& dh)
    : p(dh.p),
      houseName("copy-constructed " + 
        dh.houseName) {
    p->attach();
    cout << "DogHouse copy-constructor: "
         << *this << endl;
  }
  DogHouse& operator=(const DogHouse& dh) {
    // Controlla auto-assegnamento:
    if(&dh != this) {
      houseName = dh.houseName + " assigned";
      // Prima cancella quello che stiamo usando:
      p->detach();
      p = dh.p; // Simile al costruttore di copia
      p->attach();
    }
    cout << "DogHouse operator= : "
         << *this << endl;
    return *this;
  }
  // Decrementa refcount, e distrugge l'oggetto condizionatamente
  ~DogHouse() {
    cout << "DogHouse destructor: " 
         << *this << endl;
    p->detach(); 
  }
  void renameHouse(const string& newName) {
    houseName = newName;
  }
  void unalias() { p = p->unalias(); }
  // Copia-su-scrittura. Ogni volta che modifichiamo
  // i contenuti del puntatore 
  // bisogna prima chiamare unalias():
  void renameDog(const string& newName) {
    unalias();
    p->rename(newName);
  }
  // ... o quando permettiamo l'accesso a qualcun altro:
  Dog* getDog() {
    unalias();
    return p; 
  }
  friend ostream&
  operator<<(ostream& os, const DogHouse& dh) {
    return os << "[" << dh.houseName 
      << "] contains " << *dh.p;
  }
}; 

int main() {
  DogHouse 
    fidos(Dog::make("Fido"), "FidoHouse"),
    spots(Dog::make("Spot"), "SpotHouse");
  cout << "Entering copy-construction" << endl;
  DogHouse bobs(fidos);
  cout << "After copy-constructing bobs" << endl;
  cout << "fidos:" << fidos << endl;
  cout << "spots:" << spots << endl;
  cout << "bobs:" << bobs << endl;
  cout << "Entering spots = fidos" << endl;
  spots = fidos;
  cout << "After spots = fidos" << endl;
  cout << "spots:" << spots << endl;
  cout << "Entering auto-assegnamento" << endl;
  bobs = bobs;
  cout << "After auto-assegnamento" << endl;
  cout << "bobs:" << bobs << endl;
  // Commentare le seguenti linee:
  cout << "Entering rename(\"Bob\")" << endl;
  bobs.getDog()->rename("Bob");
  cout << "After rename(\"Bob\")" << endl;
} ///:~

La classe Dog è l'oggetto puntato da DogHouse. Essa contiene un contatore di riferimenti e delle funzioni per controllare e leggere questo contatore. C'è un costruttore di copia in modo che possiamo costruire un nuovo oggetto Dog da uno esistente.

La funzione attach( ) incrementa il contatore dei riferimennti di un oggetto Dog per indicare che c'è un altro oggetto che lo usa. detach( ) decrementa il contatore dei riferimenti. Se il contatore arriva a zero allora nessuno lo sta usando più, così la funzione membro distrugge il suo oggetto chiamando delete this.

Prima di effettuare delle modifiche (come rinominare un Dog), bisogna assicurarsi che non si sta modificando un Dog che qualche altro oggetto sta usando. Si ottiene ciò chiamando DogHouse::unalias( ), che a sua volta chiama Dog::unalias( ). L'ultima funzione restituisce il puntatore al Dog esistente se il contatore dei riferimenti è uno (che significa che nessun altro sta puntando a questo Dog), ma duplicherà il Dog se il contatore dei riferimenti è maggiore di uno.

Il costruttore di copia, invece di creare la sua propria memoria, assegna Dog al Dog dell'oggetto sorgente. Quindi, siccome adesso c'è un oggetto in più che usa questo blocco di memoria, esso incrementa il contatore dei riferimenti chiamando Dog::attach( ).

L' operator= ha a che fare con un oggetto che è stato già creato sul lato sinistro del segno =, perciò deve prima pulire a fondo questo oggeto chiamando detach( ) per l'oggetto Dog, che distrugge il vecchio Dog se nessun altro lo sta usando. Quindi l' operator= ripete il comportamento del costruttore di copia. Notare che esso controlla prima se stiamo assegnando l'oggetto a se stesso.

Il distruttore chiama detach( ) per distruggere l'oggetto Dog, se ci sono le condizioni.

Per implementare la copia-su-scrittura, bisogna controllare tutte le azioni che scrivono il nostro blocco di memoria. Per esempio, la funzione membro renameDog( ) ci permette di cambiare i valori nel blocco di memoria. Ma prima usa unalias( ) per prevenire la modifica di un  Dog "replicato"(un Dog con più di un oggetto DogHouse che punta ad esso). E se abbiamo bisogno di produrre un puntatore a un Dog dall'interno di DogHouse, bisogna dapprima chiamare unalias( ) per questo puntatore.

main( ) testa le varie funzioni che devono lavorare correttamente per implementare il conteggio dei riferimenti: il costruttore, il costruttore di copia, l'operator= e il distruttore. Esso controlla anche la copia-suscrittura chiamando renameDog( ).

Qui c'è l'output (dopo qualche piccola riformattazione):

Creating Dog: [Fido], rc = 1
Created DogHouse: [FidoHouse] 
  contains [Fido], rc = 1
Creating Dog: [Spot], rc = 1
Created DogHouse: [SpotHouse] 
  contains [Spot], rc = 1
Entering copy-construction
Attached Dog: [Fido], rc = 2
DogHouse copy-constructor: 
  [copy-constructed FidoHouse] 
    contains [Fido], rc = 2
After copy-constructing bobs
fidos:[FidoHouse] contains [Fido], rc = 2
spots:[SpotHouse] contains [Spot], rc = 1
bobs:[copy-constructed FidoHouse] 
  contains [Fido], rc = 2
Entering spots = fidos
Detaching Dog: [Spot], rc = 1
Deleting Dog: [Spot], rc = 0
Attached Dog: [Fido], rc = 3
DogHouse operator= : [FidoHouse assigned]
  contains [Fido], rc = 3
After spots = fidos
spots:[FidoHouse assigned] contains [Fido],rc = 3
Entering auto-assegnamento
DogHouse operator= : [copy-constructed FidoHouse]
  contains [Fido], rc = 3
After auto-assegnamento
bobs:[copy-constructed FidoHouse] 
  contains [Fido], rc = 3
Entering rename("Bob")
After rename("Bob")
DogHouse destructor: [copy-constructed FidoHouse]
  contains [Fido], rc = 3
Detaching Dog: [Fido], rc = 3
DogHouse destructor: [FidoHouse assigned] 
  contains [Fido], rc = 2
Detaching Dog: [Fido], rc = 2
DogHouse destructor: [FidoHouse] 
  contains [Fido], rc = 1
Detaching Dog: [Fido], rc = 1
Deleting Dog: [Fido], rc = 0

Studiando l' output, scorrendo il codice sorgente e facendo esperimenti con il programma, si può affinare la conoscenza di queste tecniche.

Creazione automatica dell' operator=

Siccome assegnare un oggetto ad un altro oggetto dello stesso tipo è un'attività che molti si aspettano che sia possibile, il compilatore crea automaticamente type::operator=(type) se non se ne fornisce uno. Il comportamento di questo operatore imita quello del costruttore di copia creato automaticamente dal compilatore; se una classe contiene oggetti (o è ereditata da un'altra classe), l' operator= per questi oggetti viene chiamato ricorsivamente. Questo viene detto assegnamento per membro. Per esempio,

//: C12:AutomaticOperatorEquals.cpp
#include <iostream>
using namespace std;

class Cargo {
public:
  Cargo& operator=(const Cargo&) {
    cout << "inside Cargo::operator=()" << endl;
    return *this;
  }
};

class Truck {
  Cargo b;
};

int main() {
  Truck a, b;
  a = b; // Stampa: "inside Cargo::operator=()"
} ///:~

L' operator= generato automaticamente per Truck chiama Cargo::operator=.

In generale non vogliamo che il compilatore faccia questo per noi. Con classi di una certa complessità (specialmente se contengono puntatori!) è preferibile creare esplicitamente l' operator=. Se non vogliamo che gli utenti della nostra classe effettuino l'assegnamento, dichiariamo l' operator= come funzione private (non è necessario definirla a meno che non la stiamo usando nella nostra classe).

Conversione automatica di tipo

In C e C++, se il compilatore vede un'espressione o una chiamata a funzione che usano un tipo che non è proprio quello richiesto, spesso può effettuare una conversione di tipo. In C++, possiamo ottenere lo stesso effetto per i tipi definiti dall'utente, definendo delle funzioni di conversione automatica dei tipi. Queste funzioni si presentano in due forme: come tipo particolare di costruttore o come operatore sovraccaricato.

Conversione con costruttore

Se si definisce un costruttore che prende come unico argomento un oggetto (o riferimento) di un altro tipo, questo permette al compilatore di effettuare una conversione automatica di tipo. Per esempio,

//: C12:AutomaticTypeConversion.cpp
// Costruttore per la conversione di tipo
class One {
public:
  One() {}
};

class Two {
public:
  Two(const One&) {}
};

void f(Two) {}

int main() {
  One one;
  f(one); // Si aspetta un Two, riceve un One
} ///:~

Quando il compilatore vede la funzione f( ) chiamata con l'oggetto One, guarda la dichiarazione di f( ) e si accorge che il tipo richiesto è Two. Quindi vede se c'è un modo di ricavare Two da One, trova il costruttore Two::Two(One) e silenziosamente lo chiama. L'oggetto risultante di tipo Two viene passato ad f( ).

In questo caso la conversione automatica di tipo ci ha risparmiato l'onere di definire due versioni sovraccaricate di f( ). Tuttavia il costo è una chiamata nascosta a un costruttore di Two, che può importare se si è interessati all'efficienza delle chiamate ad f( ).

Prevenire la conversione con costruttore

Ci sono dei casi in cui la conversione automatica con costruttore può causare problemi. Per disattivare questa conversione si può modificare il costruttore, facendolo precedere dalla parola chiave explicit (che funziona solo con i costruttori). Usata per modificare il costruttore della classe Two nell'esempio di sopra, si ha:

//: C12:ExplicitKeyword.cpp
// Usando la parola chiave "explicit"
class One {
public:
  One() {}
};

class Two {
public:
  explicit Two(const One&) {}
};

void f(Two) {}

int main() {
  One one;
//!  f(one); // Nessuna autoconversione permessa
  f(Two(one)); // OK -- l'utente effettua la conversione
} ///:~

Rendendo explicit il costruttore di Two, si dice al compilatore di non effettuare nessuna conversione automatica usando questo particolare costruttore (altri costruttori non-explicit nella stessa classe possono comunque effetture la conversione automatica). Se l'utilizzatore vuole che la conversione avvenga, deve scrivere del codice fuori dalla classe Nel codice di sopra, f(Two(one)) crea un oggetto temporaneo di tipo Two da one, proprio come ha fatto il compilatore nella versione precedente dell'esempio.

Conversione con operatore

Il secondo modo per ottenere la conversione automatica è attraverso un operatore sovraccaricato. Si può creare una funzione membro che prende il tipo corrente e lo converte in quello desiderato usando la parola chiave operator seguita dal tipo verso cui vogliamo fare la conversione. Questa forma di sovraccaricamento di operatore è unica, in quanto si vede specificato il valore di ritorno – il valore di ritorno è il nome dell'operatore che stiamo sovraccaricando. Qui c'è un esempio:

//: 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);  // Chiama Three(1,0)
} ///:~

Con la tecnica del costruttore è la classe di destinazione che effettua la conversione, mentre con la tecnica dell'operatore è la classe sorgente a fare la conversione. Il pregio della tecnica del costruttore è che si possono aggiungere nuovi percorsi di conversione ad un sistema esistente quando si crea una nuova classe. Tuttavia creando un costruttore con un solo argomento si definisce sempre una conversione automatica di tipo (anche nel caso di più argomenti, se agli altri vengono lasciati i valori di default), che potrebbe non essere la cosa che si vuole (in qual caso si può disattivare la conversione con explicit). In più, non c'è la possibilità di usare la conversione con costruttore da un tipo definito dall'utente ad un tipo predefinito; questo è possibile solo con il sovraccaricamento di operatore.

Riflessività

Uno dei principali motivi di convenienza nell'usare operatori globali sovraccaricati invece di operatori membri di classe è che nelle versioni globali la conversione automatica di tipo può essere applicata ad entrambi gli operandi, mentre con gli oggetti membro l'operando di sinistra deve essere sempre del tipo giusto. Se si vuole che entrambi gli operandi siano convertiti, la versione globale fa risparmiare un sacco di codice. Qui c'è un piccolo esempio:

//: 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; // 2nd argomento convertito a Number
//! 1 + a; // Sbagliato! il primo argomento non è di tipo Number
  a - b; // OK
  a - 1; // 2nd argomento convertito a Number
  1 - a; // 1mo argomento convertito a Number
} ///:~

La classe Number ha sia un operator+ come membro, sia un friend operator–. Siccome c'è un costruttore che prende come unico argomento un int, un int può essere automaticamente convertito a Number, ma solo alle giuste condizioni. In main( ), possiamo vedere che aggiungere un Number ad un altro Number funziona bene perchè c'è una corrispondenza esatta con l'operatore sovraccaricato. Quando il compilatore vede un Number seguito da un + e da un int int, può trovare la corrispondenza con la funzione membro Number::operator+ e convertire l'argomento int a Number usando il costruttore. Ma quando vede un int, un + e un Number, non sa cosa fare perchè tutto quello che ha è Number::operator+, che richiede che l'operando di sinistra sia già un oggetto di tipo Number. Cosicchè il compilatore produce un errore.

Con il friend operator–, le cose sono diverse. Il compilatore ha bisogno di inserire entrambi gli argomenti, per quanto può; non è costretto ad avere un tipo Number come argomento sul lato sinistro. Così, se vede

1 – a

può convertire il primo argomento a Number usando il costruttore.

A volte si vuole limitare l'uso dei propri operatori, rendendoli membri di classe. Per esempio, quando si moltiplica una matrice per un vettore, il vettore deve stare sulla destra. Ma se si vuole che gli operatori convertano entrambi gli argomenti, bisogna definire l'operatore come funzione friend.

Fortunatamente il compilatore se vede 1 – 1 non converte gli argomenti a oggetti di tipo Number per poi chiamare l' operator–. Questo significherebbe che il codice C esistente si troverebbe improvvisamente a funzionare in maniera diversa. Il compilatore cerca dapprima il confronto più "semplice", che è l'operator predefinito nell'espressione 1 – 1.

Esempio di conversione di tipo

Un esempio in cui la conversione automatica di tipo è estremamente utile accade con qualsiasi classe che incapsula stringhe di caratteri (in questo caso possiamo implementare la classe usando  semplicemente la classe string  del C++ Standard, perchè è semplice). Senza la conversione automatica di tipo, se si vogliono usare tutte le funzioni per la manipolazione delle stringhe presenti nella libreria Standard del C, bisogna creare una funzione membro per ognuna di esse, come questa:

//: C12:Strings1.cpp
// Nessuna autoconversione di tipo
#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());
  }
  // ... ecc., per ogni funzione in string.h
};

int main() {
  Stringc s1("hello"), s2("there");
  s1.strcmp(s2);
} ///:~

Qui viene creata solo la funzione strcmp( ), ma si può creare la funzione corrispondente per ognuna di quelle presenti in <cstring>, se necessario. Fortunatamente possiamo fornire una conversione automatica di tipo permettendo l'accesso a tutte le funzioni in <cstring>:

//: C12:Strings2.cpp
// Con auto conversione di tipo
#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); // Funzione C Standard
  strspn(s1, s2); // Qualunque funzione su string!
} ///:~

Ora qualunque funzione che prende un argomento di tipo char* può prendere anche un argomento di tipo Stringc perchè il compilatore sa come costruire un char* a partire da una Stringc.

Trappole nella conversione automatica di tipo

Siccome il compilatore deve scegliere il modo in cui effettuare silenziosamente la conversione, può cadere in errore se non la progettiamo correttamente. Una situazione semplice ed ovvia si presenta quando una classe X può convertire se stessa in un oggetto di tipo Y con un  operator Y( ). Se la classe Y ha un costruttore che prende un unico argomento di tipo X, questo rappresenta lo stesso tipo di conversione. Il compilatore ha ora due modi di effettuare la conversione da X a Y, e si può creare un'ambiguità :

//: C12:TypeConversionAmbiguity.cpp
class Orange; // Dichiarazione di Classe

class Apple {
public:
  operator Orange() const; // Converte Apple in Orange
};

class Orange {
public:
  Orange(Apple); // Converte Apple in Orange
};

void f(Orange) {}

int main() {
  Apple a;
//! f(a); // Errore: conversione ambigua
} ///:~

La soluzione ovvia al problema è di non fare ciò. Bisogna fornire una sola possibilità di conversione automatica da un tipo ad un altro.

Un problema molto più difficile da individuare accade quando si fornisce una conversione automatica verso più tipi. Questa viene detta fan-out:

//: C12:TypeConversionFanout.cpp
class Orange {};
class Pear {};

class Apple {
public:
  operator Orange() const;
  operator Pear() const;
};

//eat() sovraccaricata:
void eat(Orange);
void eat(Pear);

int main() {
  Apple c;
//! eat(c);
  // Errore: Apple -> Orange o Apple -> Pear ???
} ///:~

La classe Apple ha una conversione automatica sia verso Orange che verso Pear. La cosa insidiosa rispetto a questo è che non c'è nessun problema fino a quando qualcuno non crea due versioni sovraccaricate della funzione eat( ) (con una sola versione il codice in main( ) funziona bene).

Di nuovo, la soluzione – e la parola d'ordine generale con la conversione automatica dei tipi – è quella di fornire una singola conversione automatica da un tipo a un altro. Si possono avere conversioni verso altri tipi; ma queste possono semplicemente essere non automatiche. Si possono definire chiamate a funzioni esplicite con nomi come makeA( ) e makeB( ).

Attività nascoste

La conversione automatica dei tipi può introdurre molte più attività sottostanti di quante ci si possa aspettare. Come piccolo esercizio mentale, osserviamo questa modifica al file CopyingVsInitialization.cpp:

//: 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;
} ///:~

Non c'è nessun costruttore per creare l'oggetto Fee fee dall'oggetto Fo. Tuttavia, Fo ha una funzione di conversione automatica verso Fee. Non c'è nessun costruttore di copia per creare un oggetto di tipo Fee da un altro oggetto di tipo Fee, ma questa è una delle funzioni speciali che il compilatore è in grado di creare per noi (il costruttore di default, il costruttore di copia, l'operator=, e il distruttore possono essere automaticamente sintetizzati dal compilatore). Cosicchè per l'istruzione relativamente innocua

Fee fee = fo;

viene chiamato l' operatore di conversione di tipo e viene creato un costruttore di copia.

Usare la conversione automatica di tipo con attenzione. Come per ogni  sovraccaricamento di operatore, è eccellente quando riduce significativamente il codice da scrivere, ma non è meritevole di un uso gratuito.

Sommario

La ragione dell'esistenza del sovraccaricamento degli operatori è per quelle situazioni in cui questo rende la vita più facile. Non c'è niente di particolarmente magico riguardo a ciò; gli operatori sovraccaricati sono semplicemente delle funzioni con nomi simpatici, e le chiamate a queste funzioni vengono fatte dal compilatore quando questo riconosce la giusta corrispondenza. Ma se il sovraccaricamento non fornisce un significativo beneficio al creatore della classe o all'utente della stessa, è inutile complicare le cose con la sua aggiunta.

Esercizi

Le soluzioni agli esercizi selezionati si possono trovare nel documento elettronico The Thinking in C++ Annotated Solution Guide, disponibile su www.BruceEckel.com dietro una piccola ricompensa.

  1. Creare una semplice classe con un operator++ sovraccaricato. Provare a chiamare quest'operatore nelle forme pre- e postfissa e vedere che tipo di warning da il compilatore.
  2. Creare una semplice classe che contiene un int e sovraccaricare l' operator+ come funzione membro. Fornire anche una funzione membro print( ) che prende come argomento un ostream& e stampa su di esso ostream&. Testare la classe per mostrare che funziona correttamente.
  3. Aggiungere un operator- binario all'esercizio 2 come funzione membro. Dimostrare che si possono usare gli oggetti di questa classe in espressioni complesse come
    a + b – c.
  4. Aggiungere un operator++ e un operator—all'esercizio 2, sia la versione pre- che postfissa, in modo tale che restituiscano l'oggetto incrementato o decrementato. Assicurarsi che la versione postfissa restituisca il valore corretto.
  5. Modificare gli operatori di incremento e decremento dell'esercizio 4 in modo tale che le versioni prefisse restituiscano un riferimento non-const e le versioni postfisse restituiscano un oggetto const. Mostrare che funzionano correttamente e spiegare perchè questo potrebbe essere fatto in pratica.
  6. Cambiare la funzione print( ) nell'esercizio 2 in modo che sia la versione sovraccaricata dell' operator<< come in IostreamOperatorSovraccaricamento.cpp.
  7. Modificare l'esercizio 3 in modo tale che l' operator+ e l' operator- non siano funzioni membro. Dimostarre che queste continuino a funzionare correttamente.
  8. Aggiungere l' operator- unario nll'esercizio 2 e dimostrare che funziona correttamente.
  9. Creare una classe che contiene un singolo private char. Sovraccaricare gli operatori << e >> di iostream (come in IostreamOperatorSovraccaricamento.cpp) e testarli. Si possono testare con fstreams, stringstreams,  cin e cout.
  10. Determinare il valore della costante fittizia che il compilatore passa ad operator++ e operator--.
  11. Scrivere una classe Number che memorizza un double, e aggiungere una versione sovraccaricata per +, –, *, /, e per l'assegnamento. Scegliere i valori di ritorno per queste funzioni in modo tale che le espressioni possano essere concatenate, con un occhio all'efficienza. Scrivere un operatore di conversione automatica di tipo operator double().
  12. Modificare l'esercizio11 in modo venga usata l'ottimizzazione del valore di ritorno, se non è stato già fatto.
  13. Creare una classe che contiene un puntatore, e dimostrare che se si permette al compilatore di sintetizzare l' operator= il risultato dell'uso di questo operatore saranno puntatori che puntano alla stessa zona di memoria. Rimediare al problema definendo un proprio operator= e dimostrare che il problema viene risolto. Assicurarsi di controllare l'auto-assegnamento e gestire il caso in modo appropriato.
  14. Scrivere una classe di nome Bird che contiene un membro di tipo string e uno static int. Nel costruttore di default usare l' int per generare automaticamente un identificatore costruito nel membro di tipo string, attraverso il nome della classe (Bird #1, Bird #2, ecc.). Aggiungere un operator<< per ostreams per stampare gli oggetti Bird. Scrivere un operatore di assegnamento ( operator= ) e un costruttore di copia. Nel main( ), verificare che tutto funzioni correttamente.
  15. Scrivere una classe di nome BirdHouse che contiene un oggetto, un puntatore e un riferimento per la classe Bird dell'esercizio 14. Il costruttore prende I tre Birds come argomenti. Aggiungere un operator<< per ostreams per BirdHouse. Scrivere e disattivare l'operator= e un costruttore di copia. Nel main( ), verificare che tutto funzioni correttamente. Assicurarsi che si possano concatenare gli assegnamenti per gli oggetti di tipo BirdHouse e costruire espressioni che coinvolgono operatori multipli.
  16. Aggiungere un dato membro di tipo int sia a  Bird che a BirdHouse dell'esercizio 15. Aggiungere operatori membri +, -, *, e / che usano il membro int per effettuare operazioni sui rispettivi sui rispettivi membri. Verificare che questo funziona.
  17. Ripetere l'esercizio 16 usando operatori non-membro.
  18. Aggiungere un operator-- a SmartPointer.cpp e NestedSmartPointer.cpp.
  19. Modificare CopyingVsInitialization.cpp in modo che tutti I costruttori stampino un messaggio che spieghi cosa sta succedendo. Verificare adesso che le due forme di chiamate al costruttor-copia (la forma con l;assegnamento e quella con le parentesi) sono equivalenti.
  20. Provare a creare un operator= non-membro per una classe e vedere che tipo di messaggio da il compilatore.
  21. Creare una classe con un operatore di assegnamento che ha un secondo argomento, una string con un valore di default "op=call." Creare una funzione che assegna un oggetto di questa classe ad un'altra classe e mostrare che l'operatore di assegnamento viene chiamato correttamente.
  22. In CopyingWithPointers.cpp, rimuovere l' operator= in DogHouse e mostrare che l' operator= sintetizzato dal compilatore copia correttamente il valore di string ma effettua semplicemente una "replica" del puntatore a Dog.
  23. In ReferenceCounting.cpp, aggiungere uno static int e un int ordinario come dati membro sia a Dog che a DogHouse. In tutti I costruttori di entrambe le classi, incrementare lo static int e assegnare il risultato all' int ordinario per tenere traccia del numero di oggetti che sono stati creati. Effettuare le modifiche necessarie in modo tale che le istruzioni di stampa stampino gli identificatori di int degli oggetti coinvolti.
  24. Creare una classe contenente una string come membro dati. Inizializzare la string nel costruttore, ma non creare un costruttore di copia o un operator=. Costruire una seconda classe che ha un oggetto membro della prima classe; non creare un costruttore di copia o un operator= neanche per questa seconda classe. Dimostrare che il costruttore di copia e l' operator= vengono sintetizzati opportunamente dal compilatore.
  25. Combinare le classi in OverloadingUnaryOperators.cpp e Integer.cpp.
  26. Modificare PointerToMemberOperator.cpp aggiungendo due nuove funzioni membro alla classe Dog che non prendono nessun argomento e restituiscono un void. Creare e testareun  operator->* sovraccaricato che lavora con le due nuove funzioni.
  27. Aggiungere un operator->* a NestedSmartPointer.cpp.
  28. Creare due classi, Apple e Orange. In Apple, creare un costruttore che prende Orange come argomento. Creare una funzione che prende un Apple e chiama questa funzione con un Orange per mostrare che funziona. Adesso rendere il costruttore di Apple  explicit per dimostrare che viene impedita la conversione automatica dei tipi. Modificare la chiamata alla funzione in modo tale che la conversione venga fatta esplicitamente e pertanto ha successo.
  29. Aggiungere un operator* globale a ReflexivityInSovraccaricamento.cpp e dimostrare che è riflessivo.
  30. Creare due classi e un operator+ e le funzioni di conversioni in modo tale che l'addizione sia riflessiva per le due classi.
  31. Correggere TypeConversionFanout.cpp per creare una funzione esplicita da chiamare per effettuare la conversione dei tipi, invece di uno degli operatori di conversione automatica.
  32. Scrivere del semplice codice che usa gli operatori +, -, *, e / per dati di tipo double. Vedere come generare codice assembler con il proprio compilatore e guardare dentro al codice assembler generato per scoprire e spiegare cosa succede nel dettaglio.


[49] Rob Murray, C++ Strategies & Tactics, Addison-Wesley, 1993, pagina 47.

[ Capitolo Precedente ] [ Indice generale ] [ Indice analitico ] [ Prossimo Capitolo ]
Ultima modifica:24/12/2002