La segunda forma de producir conversiones automáticas de tipos es a través de la sobrecarga de operadores. Puede crear un método que tome el tipo actual y lo convierta al tipo deseado usando la palabra reservada operator seguida del tipo al que quiere convertir. Esta forma de sobrecarga de operadores es única porque parece que no se especifica un tipo de retorno -- el tipo de retorno es el nombre del operador que está sobrecargando. He aquí un ejemplo:
//: 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); // Calls Three(1,0) } ///:~
Listado 12.20. C12/OperatorOverloadingConversion.cpp
Con la técnica del constructor, la clase destino realiza la conversión, pero con los operadores, la realiza la clase origen. La ventaja de la técnica del constructor es que puede añadir una nueva ruta de conversión a un sistema existente al crear una nueva clase. Sin embargo, crear un constructor con un único argumento siempre define una conversión automática de tipos (incluso si tiene más de un argumento, siempre que el resto de los argumentos tengan valores por defecto), que puede no ser lo que desea (en cuyo caso puede desactivarla usando explicit). Además, no hay ninguna forma de usar una conversión por constructor desde un tipo definido por el usuario a un tipo predefinido; eso sólo es posible con la sobrecarga de operadores.
Una de las razones más convenientes para usar operadores sobrecargados globales en lugar de operadores miembros es que en la versión global, la conversión automática de tipos puede aplicarse a cualquiera de los operandos, mientras que con objetos miembro, el operando de la parte izquierda debe ser ya del tipo apropiado. Si quiere que ambos operandos sean convertidos, la versión global puede ahorrar un montón de código. He aquí un pequeño ejemplo:
//: 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 arg converted to Number //! 1 + a; // Wrong! 1st arg not of type Number a - b; // OK a - 1; // 2nd arg converted to Number 1 - a; // 1st arg converted to Number } ///:~
Listado 12.21. C12/ReflexivityInOverloading.cpp
La clase Number tiene tanto un operator+ miembro como un operator- friend. Dado que hay un constructor que acepta un único argumento int, se puede convertir un int automáticamente a Number, pero sólo bajo las condiciones adecuadas. En main(), puede ver que sumar un Number a otro Number funciona bien dado que tiene una correspondencia exacta con el operador sobrecargado. Además, cuando el compilador ve un Number seguido de un + y de un int, puede hacerlo corresponder con el método Number::operator+ y convertir el argumento int a Number usando el constructor. Pero cuando ve un int, un + y un Number, no sabe qué hacer porque todo lo que tiene es Number::operator+, que requiere que el operando de la izquierda sea ya un objeto Number. Así que, el compilador genera un error.
Con el operator- friend las cosas son diferentes. El compilador necesita rellenar ambos argumentos como pueda; no está restringido a tener un Number como argumento de la parte izquierda. Así que si ve:
1 - a
puede convertir el primer argumento a Number usando el constructor.
A veces querrá ser capaz de restringir el uso de sus operadores haciéndolos métodos. Por ejemplo, cuando multiplique una matriz por un vector, el vector debe ir a la derecha. Pero si quiere que sus operadores sean capaces de convertir cualquier argumento, haga que el operador sea una función friend.
Afortunadamente, el compilador no tomará la expresión 1-1 para convertir ambos argumentos a objetos Number y después llamar a operator-. Eso significaría que el código C existente podría empezar de repente a funcionar de forma diferente. El compilador intenta primero la correspondencia «más simple», es decir, en este caso el operador predefinido para la expresión 1-1.