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. Estas restricciones existen para prevenir que la creación de nuevos operadores ofusquen el significado en lugar de clarificarlo.

Las siguientes dos subsecciones muestran ejemplos de todos los operadores normales, sobrecargados en la forma habitual.

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 expandirán la clase Integer vista previamente y añadirá una nueva clase byte. El significado de sus operadores particulares dependerá de la forma en que los use, pero considere a los programadores del grupo 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 clases expuestas anteriormente (y las que siguen en la siguiente sección) son las típicas, así que empiece con ellas como un patrón cuando sobrecargue sus propios operadores.

Incremento y decremento

Los operadores de incremento++ y de decremento -- provocan un conflicto porque querrá ser capaz de llamar diferentes funciones dependiendo de si aparecen antes (prefijo) o después (postfijo) del objeto sobre el que actúan. La solución es simple, pero la gente a veces lo encuentra un poco confusa inicialmente. 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 los dos tipos, 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 diferentes firmas, así que conectan con dos cuerpos diferentes. El compilador pasa un valor constante ficticio para el argumento int (el cual nunca se proporcionada por un identificador porque el valor nunca se usa) para generar las diferentes firmas para la versión postfija.