12.3. Operadores sobrecargables

Aunque puede sobrecargar casi todos los operadores disponibles en C, el uso de operadores sobrecargados es bastante restrictivo. En particular, no puede combinar operadores que actualmente no tienen significado en C (como ** para representar la potencia), no puede cambiar la precedencia de evaluación de operadores, y tampoco el número de argumentos requeridos por un operador. Tiene sentido: todas esas acciones producirían operadores que oscurecerían el significado en lugar de aclararlo.

Las siguientes dos subsecciones muestran ejemplos de todos los operadores «normales», sobrecargados en la forma que probablemente usará.

12.3.1. Operadores unarios

El siguiente ejemplo muestra la sintaxis para sobrecargar todos los operadores unarios, en ambas formas: como funciones globales (funciones friend, no métodos) y como métodos. Estas funciones amplían la clase Integer vista previamente y añaden una nueva clase byte. El significado de sus operadores particulares dependerá de la forma en que los use, pero piense en el programador cliente antes de hacer algo inesperado. He aquí un catálogo de todas las funciones unarias:

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

// Non-member functions:
class Integer {
  long i;
  Integer* This() { return this; }
public:
  Integer(long ll = 0) : i(ll) {}
  // No side effects takes const& argument:
  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);
  // Side effects have non-const& argument:
  // Prefix:
  friend const Integer&
    operator++(Integer& a);
  // Postfix:
  friend const Integer
    operator++(Integer& a, int);
  // Prefix:
  friend const Integer&
    operator--(Integer& a);
  // Postfix:
  friend const Integer
    operator--(Integer& a, int);
};

// Global operators:
const Integer& operator+(const Integer& a) {
  cout << "+Integer\n";
  return a; // Unary + has no effect
}
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 is recursive!
}
int operator!(const Integer& a) {
  cout << "!Integer\n";
  return !a.i;
}
// Prefix; return incremented value
const Integer& operator++(Integer& a) {
  cout << "++Integer\n";
  a.i++;
  return a;
}
// Postfix; return the value before increment:
const Integer operator++(Integer& a, int) {
  cout << "Integer++\n";
  Integer before(a.i);
  a.i++;
  return before;
}
// Prefix; return decremented value
const Integer& operator--(Integer& a) {
  cout << "--Integer\n";
  a.i--;
  return a;
}
// Postfix; return the value before decrement:
const Integer operator--(Integer& a, int) {
  cout << "Integer--\n";
  Integer before(a.i);
  a.i--;
  return before;
}

// Show that the overloaded operators work:
void f(Integer a) {
  +a;
  -a;
  ~a;
  Integer* ip = &a;
  !a;
  ++a;
  a++;
  --a;
  a--;
}

// Member functions (implicit "this"):
class Byte {
  unsigned char b;
public:
  Byte(unsigned char bb = 0) : b(bb) {}
  // No side effects: const member function:
  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;
  }
  // Side effects: non-const member function:
  const Byte& operator++() { // Prefix
    cout << "++Byte\n";
    b++;
    return *this;
  }
  const Byte operator++(int) { // Postfix
    cout << "Byte++\n";
    Byte before(b);
    b++;
    return before;
  }
  const Byte& operator--() { // Prefix
    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);
} ///:~

Listado 12.2. C12/OverloadingUnaryOperators.cpp


Las funciones están agrupadas de acuerdo a la forma en que se pasan los argumentos. Más tarde se darán unas cuantas directrices de cómo pasar y devolver argumentos. Las formas expuestas anteriormente (y las que siguen en la siguiente sección) son las que normalmente usará, así que tómelas como patrón cuando sobrecargue sus propios operadores.

Incremento y decremento

Los operadores de incremento ++ y de decremento -- plantean un dilema porque querrá poder llamar a funciones diferentes dependiendo de si aparecen antes (prefijo) o después (postfijo) del objeto sobre el que actúan. La solución es simple, aunque a la gente a veces le resulta un poco confusa al principio. Cuando el compilador ve, por ejemplo, ++a (un pre-incremento), genera una llamada al operator++(a) pero cuando ve a++, genera una llamada a operator++(a, int). Así es como el compilador diferencia entre las dos formas, generando llamadas a funciones sobrecargadas diferentes. En OverloadingUnaryOperators.cpp para la versión de funciones miembro, si el compilador ve ++b, genera una llamada a B::operator++() y si ve b++ genera una llamada a B::operator++(int).

Todo lo que el usuario ve es que se llama a una función diferente para las versiones postfija y prefija. Internamente, sin embargo, las dos llamadas de funciones tienen prototipos diferentes, así que se enlazan con dos cuerpos de función diferentes. El compilador pasa un valor constante ficticio para el argumento int (al que nunca se le da un identificador porque el valor nunca se usa) para generar un prototipo diferente para la versión postfija.