Vous êtes sur la page 1sur 25

5 Punteros inteligentes

Debemos resaltar que en el programa anterior disponemos de dos formas de acceso indirecto a los
miembros del objeto v1:
cout << v1->x; // a
cout << vptr->x; // d

Hemos indicado que ambas utilizan la versin global del selector -> sobre el miembro x como
segundo operando. Pero existe una diferencia crucial: la forma a permite introducir una funcin
previa, representada por operator->( ), lo que abre todo un mundo de posibilidades.
En realidad, este comportamiento atpico de la funcin operator->( ), que hemos visto se aparta
del resto de operadores, no es arbitraria. Representa la puerta de acceso a lo que se
denominan punteros inteligentes; objetos que actan como punteros, pero que adems pueden
realizar alguna accin previa cada vez que un objeto es accedido a travs de ellos. Habida cuenta
que esta accin previa puede ser cualquiera (todo lo que pueda hacer una funcin), los punteros
inteligentes permiten tcnicas de programacin muy interesantes.
La idea puede ser concretada en tres formas bsicas que comentamos separadamente:
1. Incluir la funcin operator-> en la definicin de la clase
2. Incluir la funcin operator-> en una clase independiente
3. Incluir la funcin oprator-> en una clase anidada

5.1 Incluir la funcin operator->( ) en la definicin de la clase (es el caso del ejemplo anterior):
class Vector {
public: int x, y;
Vector* operator-> () {
/* funcionalidad adicional requerida */
return this;
}
};
Como se ha visto, este diseo permite que los objetos de la clase Vector puedan ser accedidos
indirectamente:
Vector v1;
v1->x = 2; v1->y = 4;

5.2 Incluir la funcin operator->( ) en una clase Vptr independiente:
class Vector {
...
};

class Vptr {
...
Vector* vpt;
Vector* operator->() {
/* funcionalidad adicional requerida */
return vpt;
}
};

En este caso los objetos Vptr pueden ser utilizados para acceder a los de clase Vector, de forma
parecida a como se utilizan los punteros. Lo ilustramos con un ejemplo compilable:
#include <iostream>
using namespace std;

class Vector { public: int x, y; };

class Vptr {
public:
class Vector* vpt;
Vector* operator->() {
cout << "Acceso a vector (" << vpt->x << ", " << vpt->y << ")" <<
endl;
return vpt;
}
};

void main() { // =====================
Vector v1 = {2, 1}; // objeto tipo vector inicializado
Vptr vpt = { &v1 }; // objeto Vptr que seala a v1
cout << "v1.x == " << vpt->x << endl;
vpt->y = 4;
cout << "v1.y == " << vpt->y << endl;
}
Salida:
Acceso a vector (2, 1)
v1.x == 2
Acceso a vector (2, 1)
Acceso a vector (2, 4)
v1.y == 4

5.3 Incluir la funcin operator->( ) en una clase Vptr contenida (anidada) en Vector:
class Vector {
...
class Vptr {
...
Vector* operator->() {
/* funcionalidad adicional requerida */
return vpt;
}
};
};

Si los objetos Vptr no tienen sentido como entes independientes de los objetos Vector, o no
pueden ser utilizados para otras clases, esta disposicin tambin puede ser vlida y en cierta
forma equivalente al diseo anterior.
Veamos este diseo en un ejemplo concreto:
#include <iostream>
using namespace std;

class Vector {
public: int x, y;
class Vptr {
public:
Vector* vpt;
Vector* operator-> () {
cout << "Accedido vector (" << vpt->x << ", " << vpt->y << ")" <<
endl;
return vpt;
}
} p1;
};

void main() { // ==============
Vector v1 = {1, 2};
v1.p1.vpt = &v1; // M.2
cout << "v1.x == " << v1.p1->x << endl; // M.3
v1.p1->y = 4;
cout << "v1.y == " << v1.p1->y << endl;
}
Como puede suponerse, la salida que se obtiene es idntica a la del ejemplo anterior.
Observe en M.2 la inicializacin del puntero vpt; un miembro del objeto p1. (miembro a su vez del
objeto v1). Esta circunstancia, miembros de objetos que pueden tener a su vez propiedades y
mtodos, es caracterstica de las clases cuyos miembros son a su vez instancias de otras clases.
En este caso, de la clase Vptr definida dentro de Vector.
Observe tambin que en M.3 y siguientes se invoca el mtodo operator-> del miembro p1 del
objeto v1.

5.3.2 Aunque la versin anterior es totalmente funcional, su diseo nos obliga a inicializar el
puntero vpt para cada instancia de la clase Vector(como se ha hecho en M.2). En el ejemplo que
sigue refinamos ligeramente el diseo anterior aadindole un constructor, al objeto de evitar tener
que realizar esta inicializacin cada vez.
#include <iostream>
using namespace std;

class Vector {
public: int x, y;
class Vptr {
public:
Vector* vpt;
Vector* operator-> () {
cout << "Accedido vector (" << vpt->x << ", " << vpt->y << ")" <<
endl;
return vpt;
}
} p1;
Vector (int i = 0, int j = 0) { // L.14: constructor
x = i; y = j;
p1.vpt = this;
}
};

void main() { // ================
Vector v1 = Vector(2, 1); // M.1
// v1.p1.vpt = &v1; ya no es necesaria M.2
cout << "v1.x == " << v1.p1->x << endl;
v1.p1->y = 4;
cout << "v1.y == " << v1.p1->y << endl;
}
La salida es tambin idntica a la de los dos anteriores. El cambio introducido se limita casi
exclusivamente a la inclusin de un constructor explcito (L.14) para la clase Vector.
Como puede comprobarse, la inicializacin M.2 ya no es necesaria; se ha encomendado esta
funcin al constructor. Ntese la utilizacin explcita del puntero this ( 4.11.6) para este
cometido.
Observe tambin que la existencia de un constructor explcito nos ha obligado a modificar
ligeramente la sintaxis de la sentencia M.1 en la que creamos e inicializamos el objeto v1 (
4.11.2d3).
En cualquier caso, cualquiera que sea el diseo adoptado para la "sobrecarga" del selector
indirecto, debemos conservar la idea central: estos punteros inteligentes permiten la utilizacin de
un cdigo cuya ejecucin es previa al acceso al objeto. En los ejemplos anteriores dicho cdigo se
ha concretado en una salida mostrando el estado actual (componentes) del vector.
4.9.18f Sobrecarga del operador de invocacin de funcin
1 Antecedentes:
La invocacin de funciones ( 4.4.6) en C++ tiene la siguiente sintaxis general:
expresin-postfija ( <lista-de-expresiones> );

1.1 En su utilizacin normal, expresin-postfija es el nombre de una funcin, un puntero a
funcin o la referencia a una funcin. Por ejemplo:
float sum(int i, int j) {
float s = i + j;
cout << "La suma es: " << s << endl;
return s;
}
...
float (*fptr)(int, int) = sum; // definicin de puntero-a-funcin
float (&fref)(int, int) = sum; // definicin de referencia-a-funcin
int x = 2, y = 5;
sum(x*2, y); // Ok. invocacin
fptr(x*2, y); // Ok. invocacin
fref(x*2, y); // Ok. invocacin

1.2 Cuando se utiliza con funciones-miembro (mtodos), expresin-postfija es el nombre
de un mtodo, una expresin de puntero-a-clase (utilizado para seleccionar un mtodo), o de
puntero-a-miembro. Por ejemplo:
class Vector { // una clase cualquiera
float x, y;
public: void getm(int i) { // funcin-miembro (mtodo)
cout << "Vector: (" << x * i << ", " << y * i << ") " << endl;
}
};
...
Vector v1; // Objeto
Vector* vptr = &v1; // definicin de puntero-a-clase
void (Vector::* vmptr) (int) // definicin de puntero-a-miembro
= &Vector::getm;
int x = 2;
v1.getm(x); // Ok. invocacin del mtodo
vptr->getm(x); // Ok. dem.
(v1.*vmptr)(x); // Ok. dem.

En este contexto nos referimos al parntesis ( ) como operador de invocacin de funcin (
4.9.16), aunque sabemos que tiene otros usos en el lenguaje: servir de signo de puntuacin (
3.2.6) y delimitador en algunas expresiones. Por ejemplo, las expresiones con coma (
4.10.5).
2 Sinopsis:
La gramtica C++ permite definir una funcin-miembro no esttica operator( ) cuya definicin sea
del tipo [1]:
valor-devuelto operator( )( <lista-de-argumentos> ) { /* definicin */
} // 2a
En este caso, las instancias de clases en que se han definido estos mtodos, presentan una
curiosa peculiaridad sintctica: que sus mtodosoperator() pueden ser invocados utilizando
directamente el identificador del objeto como expresin-postfija es decir:
obj( <lista-de-argumentos> );
siendo obj una instancia de la clase Cl para la que se define la funcin operator( ). Por ejemplo:
class Cl {
public:
...
void operator()(int x) { /* definicin */ }
...
};

...
Cl obj;
obj(5); // Ok!!
Cuando se utiliza esta notacin, el compilador transforma la expresin anterior en una invocacin
a operator( ) en la forma cannica:
obj.operator()( <lista-de-argumentos> ); // 2b
Observe que se trata simplemente de la invocacin de una funcin-miembro sobre el objeto obj, y
que nada impide que sea invocada directamente al modo tradicional con la sintaxis cannica
2b . Es decir, utilizando explcitamente la sustitucin realizada por el compilador.
No confundir la expresin anterior (2a ) con la utilizacin de operator( ) como operador de
conversin ( 4.9.18k), donde se utiliza sin especificacin del valor devuelto y sin que pueda
aceptar ningn tipo de parmetro:
operator( ){ /* valor devuelto */ } // 2c
Recordemos que operator( ) puede aparecer con dos significados en el interior de una clase:
class C {
valor-devuelto operator()(argumentos); // operador de invocacin a
funcin
operator() { /* ... */ } // operador de conversin
...
};

2.1 Lo ilustramos con un ejemplo:
#include <iostream>
using namespace std;

class Vector {
public:
float x, y;
void operator()() { // funcin-operador
cout << "Vector: (" << x << ", " << y << ") " << endl;
}
};

void main () { // =================
Vector v1 = {2, 3};
v1(); // Ok. invocacin de v1.operator()
v1.operator()(); // Ok. invocacin clsica
}
Salida
Vector: (2, 3)
Vector: (2, 3)
3 El operador de invocacin de funcin no es sobrecargable
Respecto a la "sobrecarga" del operador de invocacin de funcin ( ), podemos decir algo anlogo
a lo indicado para la sobrecarga del selector indirecto ->; ( 4.9.18e): a pesar de que en la
literatura sobre el tema, la descripcin de la funcin operator( ) se encuentra siempre en el
captulo dedicado a la sobrecarga de operadores, en realidad no se trata de tal sobrecarga. Al
menos no en un sentido homogneo al empleado con el resto de opradores. Hemos visto que se
trata de una mera curiosidad sintctica; una forma algo extraa de invocacin de determinadas
funciones-miembro (cuando estas funciones responden a un nombre especial).
Al hilo de lo anterior, y dado que el identificador operator( ) es nico, resulta evidente que si se
definen varias de estas funciones, se aplicar la congruencia estndar de argumentos ( 4.4.1a)
para resolver cualquier ambigedad. Por ejemplo:
#include <iostream>
using namespace std;

class Vector {
public: float x, y;
void operator()() { // L.6 Versin-1
cout << "Vector: (" << x << ", " << y << ") " << endl;
}
void operator()(int i) { // L.9 Versin-2
cout << "Coordenadas: (" << x << ", " << y << ") " << endl;
}
};

void main () { // ============
Vector v1 = {2, 3};
v1(); // Ok. invoca versin-1 b
v1(1); // Ok. invoca versin-2 c
}
Salida:
Vector: (2, 3)
Coordenadas: (2, 3)
Comentario
Observe que en L.9, el argumento (int) de la segunda definicin se ha utilizado exclusivamente
para permitir al compilador distinguir entre ambas. Esta tcnica ya la hemos visto en la sobrecarga
de los post-operadores incremento y decremento ( 4.9.18c).
4 Objetos-funcin
Lo indicado hasta aqu podra parecer un mero capricho sintctico del creador del lenguaje; una
forma particular de invocacin de ciertas funciones-miembro (de nombre especial), que presentan
la singularidad de permitir utilizar objetos como si fuesen funciones (caso de las expresiones b y
c ), pero que no tienen una justificacin objetiva, ya que no resuelve un problema que no pueda
ser resuelto con alguno de los recursos existentes en el lenguaje.
Precisamente, en razn de que pueden ser utilizadas como funciones, las instancias de clases
para las que se han definido funciones operator( ), reciben indistintamente el nombre de objetos-
funcin, funciones-objeto [2] o functor, y algn autor ha definido a estas entidades como "datos
ejecutables" [3].
En realidad, como ocurre con otros detalles de su diseo, este aparente capricho sintctico
encierra un mundo de sutilezas. Su importancia y razn de ser estriban en que permite escribir
cdigo que realiza operaciones complejas a travs de argumentos de funciones ( 5.1.3a1).
Precisamente la Librera Estndar de Plantillas C++ (STL 5.1) o sus extensiones, como las
libreras Boost, donde se encuentran algunos de los conceptos y algoritmos ms sofisticados que
haya construido hasta el momento la ingeniera de software, utiliza con profusin este tipo de
recursos.
Recuerde que los objetos (instancias de clases) pueden ser pasados como argumentos de
funciones y que sus mtodos pueden acceder a las propiedades de la clase, de forma que pasar
un objeto-funcin como argumento, equivale a pasar a la funcin ms informacin de la que
supondra un escalar. Esta circunstancia tiene muchas aplicaciones. Por ejemplo, la nueva versin
del Estndar permitir crear en una aplicacin un hilo ("thread") de ejecucin mediante una
expresin del tipo:
void work_to_do(); // L.1
std::thread newThread (work_to_do); // L.2
La funcin work_to_do() define el proceso que se ejecutar en el nuevo hilo representado por el
objeto newThread de L.2. Sin embargo, el constructor de la clase std::trhead exige como
argumento un puntero a funcin que no recibe argumentos y devuelve void. De forma que no es
posible pasar en el constructor ninguna informacin adicional sobre detalles de la tarea a realizar y
es en este punto, donde las caractersticas de C++ vienen al rescate, porque al igual que muchos
otros algoritmos de la Librera Estndar C++, el argumento no tiene porqu ser necesariamente
una funcin ordinaria; tambin puede ser un objeto-funcin, as que el diseo podra ser como
sigue:
class Work_to_do {
public:
// miembros que representan particularidades del proceso
int a_, b_;
// miembro que representa el resultado del proceso
int& c_;
// constructor que permite fijar las caractersticas
Work_to_do (int a, int b, int& c) : a_(a), b_(b), c_(c) {}

// operador de invocacin a funcin
void operator()() { c_ = a_ + b_; }
};
...
int r;
std::thread newThread (Work_to_do(1,2,r));
Observe que el argumento Work_to_do(x,y,z) es una llamada al constructor de la clase, que
genera un objeto-funcin; que a su vez, es pasado al constructor de la clase std::trhead para
construir el objeto newThread (que representa el nuevo hilo). Una vez concluida la tarea, el
resultado lo obtenemos en r.
4.1 Unin de argumentos
Observe que en el ejemplo anterior, el recurso ha consistido en empaquetar los argumentos
involucrados en un objeto-funcin. Esta tcnica, conocida como unin o empaquetado de
argumentos ("argument binding") es ampliamente utilizada, aunque tal como la hemos
presentado, tiene el inconveniente de que hay que preparar manualmente la clase adecuada. Sin
embargo, si como suele ser frecuente [4] la situacin se repite, es posible automatizarla utilizando
una plantilla.
Supongamos que el proceso que deba ejecutar la hebra ("thread") pueda ser definida
genricamente mediante una funcin del tipo
void work_to_do (A a, B b, C& c);
En la que los objetos a y b representan los datos particulares y c la variable en la que se obtiene
el resultado. En estas circunstancias, es posible definir una clase genrica ( 4.12.2) tal como:
template <typename A, typename B, typename C> class bind {
public:
A a_;
B b_;
C& c_;
void (*pf)(A,B,C&);
bind (void (*p)(A,B,C&), A a, B b, C& c)
: pf(p), a_(a), b_(b), c_(c) {}

void operator()() { pf(a_, b_, c_); }
};
Suponiendo que tenemos definida la funcin que realiza el proceso en un caso concreto:
void process1 (int a, char b, float& c) {
/* proceso a realizar por la hebra
el resultado es situado en c */
...
}
Para lanzar una hebra que realizara esa tarea, solo tendramos que incluir un par de lenas en
nuestro cdigo:
float f;
std:thread tread1 (bind (process1, 2, 'c', f));

4.9.18g Sobrecarga de operadores lgicos
1 Sinopsis:
Recordemos ( 4.9.8) que los operadores lgicos suelen ser representados por sus nombres en
ingls (maysculas): AND (&&); OR (||) y NOT (!). Los dos primeros son binarios, mientras que el
ltimo es unario, lo que significa que AND y OR aceptan dos argumentos, mientras que la negacin
NOT, acepta solo uno. Recordemos tambin que los operandos de las versiones globales de estos
operadores son convertidos a un tipobool, y que el resultado es tambin un tipo bool de valor
cierto/falso (true/false).
Cualquier intento de aplicar estos operadores a tipos abstractos ( 2.2) genera un error de
compilacin recordndonos que la operacin solo est definida para los tipos bsicos
(preconstruidos en el lenguaje). La solucin en estos casos es sobrecargar adecuadamente estos
operadores para los miembros de la clase, lo que puede realizarse mediante los procedimientos
estndar ya sealados para operadores unarios ( 4.9.18c) y binarios ( 4.9.18b).
2 Permanencia de las leyes formales
Antes de exponer algunos ejemplos, recordemos los preceptos que hemos denominado de
permanencia de las leyes formales ( 4.9.8) que son especialmente pertinentes en estos casos
de sobrecarga.
Hemos sealado que la sobrecarga permite al programador una gran libertad, de forma que puede
cambiar totalmente la funcionalidad del operador respecto a la que tiene con los tipos bsicos. Por
ejemplo, podemos definir el operador AND entre miembros c1 y c2 de una clase C de forma que
devuelva un valor de cualquier tipo en vez de un booleano, aunque lo lgico sera que fuese
un bool, con objeto que el resultado de estas operaciones fuese el que se espera intuitivamente.
Adems de esto, considere que los tres operadores estn relacionados desde el punto de vista
lgico, y deberan seguir estndolo de la misma forma en las versiones sobrecargadas.
Por ejemplo, si definimos la sobrecarga del operador de negacin lgica NOT de forma que para
los objetos c1 y c2 resulta:
!c1 == false
!c2 == false
Deberamos definir la sobrecarga del operador AND de forma que
(c1 && c2) == true
De lo contrario estaramos construyendo para los objetos de tipo C una lgica bastante difcil de
comprender a una mente acostumbrada al razonamiento estndar con los tipos bsicos del
lenguaje.
En este captulo trataremos de demostrar que si se quiere mantener una lgica coherente, la
sobrecarga de los tres operadores lgicos AND, OR y NOT para tipos de una clase C, puede ser
sustituida por una conversin de usuario mediante una funcin de conversin operator
bool() adecuada ( 4.9.18k).
3 Sobrecarga del operador NOT
El operador NOT de negacin lgica ( ! ) est relacionado con su contrario (que no tiene nombre ni
representacin). Para explicar el significado de esta afirmacin, supongamos un objeto c de una
clase C. Segn hemos sealado, un intento de utilizarlo, por ejemplo, la expresin:
if (!c) { /* ... */ }
genera un error de compilacin: 'operator!' not implemented in type 'C'..., en el
que se indica que el operador NOT de negacin lgica no est definido para objetos de la clase.
Sin embargo, podemos observar que un intento anlogo sin el operador:
if (c) { /* ... */ }
tambin produce un error, aunque en este caso la indicacin es ms ambiga: Illegal
structure operation in function...(Borland) o: conditional expression of type
'class C' is illegal (Visual C++). En este ltimo caso el compilador est indicando que no
sabe como convertir la expresin entre parntesis (c) a un tipo bool. Recuerde que la
sentencia if(<condicion>)... espera recibir una expresin <condicion> que se resuelva en
un bool ( 4.10.2).

3.1 Ambos inconvenientes pueden resolverse adoptando las medidas pertinentes. El primero
sobrecargando el operador NOT para objetos de la clase. El segundo proporcionando una
conversin de usuario que permita al compilador transformar un tipo C en un bool ( 4.9.18k).
Por ejemplo, supongamos una clase V2D para contener puntos de un plano definidos por sus
coordenadas cartesianas. Para ciertas operaciones lgicas con estos objetos, consideramos
"ciertos" los puntos que pertenecen al primer cuadrante y "falsos" todos los dems. Un posible
diseo sera el siguiente:
class V2D { // clase de puntos en el plano
float x, y;
public:
V2D(float i=0, float j=0): x(i), y(j) {} // constructor
bool operator!() { // sobrecarga del operador NOT
return ((x > 0 && y > 0) ? false : true );
}
operator bool() { // conversin de usuario
return ((x > 0 && y > 0) ? true : false );
}
};
...
void func () {
V2D p1(0,2);
V2D p2(-1.1, 2);
V2D p3 = Vector2D(1,2);
V2D p4(1, -3);

if (p1) cout << "p1 Ok.";
if (p2) cout << "p2 Ok.";
if (p3) cout << "p3 Ok.";
if (p4) cout << "p4 Ok."; // p3 Ok.

if (!p1) cout << "p1 Not Ok."; // p1 Not Ok.
if (!p2) cout << "p2 Not Ok."; // p2 Not Ok.
if (!p3) cout << "p3 Not Ok."; // p3 Not Ok.
if (!p4) cout << "p4 Not Ok.";
}
Observe que la definicin de operator bool debe ser congruente con la definicin de operator!, de
forma que un objeto no pueda ser cierto y falso al mismo tiempo.
3.2 Observe tambin que una vez definida la funcin de conversin operator bool, la de
negacin no es realmente necesaria. En efecto: la definicin
class V2D { // clase de puntos en el plano
float x, y;
public:
V2D(float i=0, float j=0): x(i), y(j) {} // constructor
operator bool() { // conversin de usuario
return ((x > 0 && y > 0) ? true : false );
}
};
produce exactamente las mismas salidas que la anterior. La razn es que en las expresiones (!
p) el objeto p es convertido a tipo bool por accin de la versin global del propio operador NOT.
Esta conversin se realiza mediante una invocacin del tipo p.operator bool().

3.3 Un ltimo truco podra permitirnos la operatoria inversa: obtener los valores (p) mediante
una doble negacin (!!p). En cuyo caso podramos eliminar la conversin de usuario operator
bool, dejando la sobrecarga del operador de negacin operator!:
class V2D { // clase de puntos en el plano
float x, y;
public:
V2D(float i=0, float j=0): x(i), y(j) {} // constructor
bool operator!() { // sobrecarga del operador NOT
return ((x > 0 && y > 0) ? false : true );
}
};
...
void func () {
V2D p1(0,2);
V2D p2(-1.1, 2);
V2D p3 = Vector2D(1,2);
V2D p4(1, -3);

if (!!p1) cout << "p1 Ok.";
if (!!p2) cout << "p2 Ok.";
if (!!p3) cout << "p3 Ok.";
if (!!p4) cout << "p4 Ok."; // p3 Ok.

if (!p1) cout << "p1 Not Ok."; // p1 Not Ok.
if (!p2) cout << "p2 Not Ok."; // p2 Not Ok.
if (!p3) cout << "p3 Not Ok."; // p3 Not Ok.
if (!p4) cout << "p4 Not Ok.";
}

4 Sobrecarga de peradores AND y OR
Es significativo que si, como en el caso anterior 3.2 , se dispone de una conversin de usuario
que garantice la conversin de un objeto c de tipo C a tipo bool, entonces no es realmente
necesario sobrecargar los operadores AND ni OR para poder utilizarlos. Por ejemplo, suponiendo
la definicin (3.2) y los vectores (3.3) anteriores, las sentencias:
if (p3 && p1) cout << "p3 y p1 Ok.";
else cout << "p3 y p1 NOT ok.";
if (p3 || p1) cout << "p3 o p1 Ok.";
else cout << "p3 o p1 NOT Ok.";
Producen las siguientes salidas:
p3 y p1 NOT ok.
p3 o p1 Ok.
La razn es la sealada en el caso del operador NOT (3.2 ). Si existe posibilidad de conversin
de los operandos a tipos bool, entonces el compilador utiliza las versiones globales de ambos
operadores una vez realizada la conversin correspondiente.

4.1 Si de todos modos es preciso sobrecargar alguno de estos operadores, el procedimiento es
el mismo que con cualquier operador binario ( 4.9.18b):
a. Declarando una funcin miembro no esttica que acepte un argumento
b. Declarando una funcin externa (generalmente friend) que acepte dos argumentos.

Como ejemplo, procederemos a la sobrecarga de los operadores AND y OR para la clase V2D ya
mencionada, que utilizamos para contener puntos de un plano. Las operaciones mantendrn
coherencia con los principios utilizados al sobrecargar el operador NOT (3.1 ). Recordemos
que para las operaciones lgicas con estos objetos, consideramos "ciertos" los puntos que
pertenecen al primer cuadrante y "falsos" todos los dems.
La sobrecarga de AND se realiza mediante el procedimiento a (una funcin miembro no esttica);
para OR se utiliza el procedimiento b (una funcin externa que acepte dos argumentos). El diseo
es el siguiente:
#include <iostream>
using namespace std;

class V2D {
public:
float x, y;
V2D(float i=0, float j=0): x(i), y(j) { } // constructor
bool operator&&(V2D& p) { // funcin-operador
if (x > 0 && y > 0) {
if (p.x > 0 && p.y > 0) return true;
}
return false;
}
friend bool operator||(V2D&, V2D&);
};

bool operator||(V2D& p1, V2D& p2) { // funcin-operador
if (p1.x > 0 && p1.y > 0) return true;
if (p2.x > 0 && p2.y > 0) return true;
return false;
}

int main() { // =================
V2D p1(0,2);
V2D p2(-1.1, 2);
V2D p3 = V2D(1,2);
V2D p4(1, -3);

if (p3 && p1) cout << "p3 y p1 Ok.\n";
else cout << "p3 y p1 NOT ok.\n";
if (p3 || p1) cout << "p3 o p1 Ok.\n";
else cout << "p3 o p1 NOT Ok.\n";
}
Salida:
p3 y p1 NOT ok.
p3 o p1 Ok.
Comentario
Observe que las funciones operator&& y operator|| utilizan la versin global del operador AND
(&&). Observe tambin que en ambas definiciones se ha respetado la mecnica descrita en el
Estndar para las versiones globales. De forma que se devuelve un resultado tan pronto como se
tiene constancia de este, sin necesidad de terminar todas las comprobaciones que puedan estar
involucradas. Por ejemplo, en la definicin deoperator||, si se cumple la primera
comprobacin (p1.x > 0 && p1.y > 0), se devuelve directamente un resultado true, sin
necesidad de esperar a realizar la segunda.
4.9.18h Sobrecarga de Enumeraciones
1 Sinopsis
El hecho de referirnos a la sobrecarga de enumeraciones ( 4.7) podra parecer en
contradiccin con lo indicado al tratar de la sobrecarga en general ( 4.9.18): que "se refiere y
tiene aplicacin solo cuando los operandos son instancias de clases". Sin embargo, hay que tener
en cuenta que las enumeraciones son en realidad un tipo muy particular de estructuras (y por
ende, de clases), que como tales, gozan de muchas de las caractersticas de aquellas [1].
2 Sobrecarga
Es posible sobrecargar la mayora de los operadores para una enumeracin, pero dado que estas
no pueden tener funciones-miembro, no es posible sobrecargar aquellos que precisamente exigen
ser sobrecargados a travs de mtodos no estticos. Concretamente los operadores =, [ ], ( ) y -
> no pueden ser sobrecargados para un enum.
La consecuencia inmediata es aadir que la sobrecarga de enumeraciones debe realizarse a
travs de funciones ordinarias (no pertenecientes a clases), y que al menos uno de los argumentos
debe ser del tipo de la enumeracin que se sobrecarga.

3 Comprobar en el ejemplo que sigue la forma de sobrecargar los operadores de preincremento y
postincremento. Observe que en cierta forma, la enumeracin se comporta como una clase.
#include <iostream>
#using amespace std;

enum ESTACION { primavera, verano, otono, invierno };
ESTACION& operator++ (ESTACION& s) { // Preincremento ++@
s = ESTACION( (s + 1) % 4 ); // L.6
return s;
}
ESTACION operator++(ESTACION& s, int) { // Postincremento @++
ESTACION tmp = s;
switch (s) {
case primavera: s = verano; break;
case verano: s = otono; break;
case otono: s = invierno; break;
case invierno: s = primavera; break;
}
return tmp;
}

int main(void) { // =============
ESTACION est = otono;
cout << "La estacion es " << est << endl;
cout << "Preincrementar la estacion: "<< ++est << endl;
cout << "No cambiar si se usa el postincremento: " << est++ << endl;
cout << "Finalmente: " << est << endl;
}
Salida:
La estacion es 2
Preincrementar la estacion: 3
No cambiar si se usa el postincremento: 3
Finalmente: 0
Comentario
Por tratarse de la sobrecarga de operadores unarios como funciones externas, ambas funciones-
operador se han definido utilizando la forma que hemos denominado "b" ( 4.9.18c):
Preincremento: funcin que acepta un argumento operator++(ESTACION)
Posincremento: funcin acepta un objeto y un entero operator++(ESTACION, int)
El preincremento recibe el argumento por referencia (L.5), de forma que modifica el valor recibido.
Observe como en L.6 se efecta el calculo aritmtico, seguido de un modelado al tipo ESTACION, y
como en L.7 este valor es devuelto por referencia.
Observe como el postincremento devuelve un objeto "por valor", y que este objeto es una copia del
objeto inicial. Simultneamente la funcin modifica el valor del objeto recibido inicialmente (por
referencia).
4.9.18k Conversiones definidas por el usuario
El presente captulo es un claro ejemplo de un tpico difcil de clasificar.
Aunque podra haber encajado igualmente bien (o mal) en otros sitios, lo
hemos incluido en el epgrafe dedicado a la sobrecarga de operadores porque
en una de sus formas se refiere a la funcin-operador operator. Este es
tambin el criterio del Dr. Stroustrup en su obra TC++PL.
1 Prembulo
Recordemos que el lenguaje C++ dispone de una serie de mecanismos de conversin para los
tipos bsicos, que son utilizados automticamente en determinadas circunstancias. Son las
conversiones estndar ( 2.2.5). El lenguaje tambin permite que puedan realizarse conversiones
implcitas o explcitas para los tipos abstractos, aunque en este caso es el programador el que
debe adoptar las medidas pertinentes, razn por la cual se denominan conversiones definidas
por el usuario ("User-defined conversions").
Existen dos formas de definir estas ltimas conversiones: mediante constructores y mediante
operadores de conversin. Ambos tipos sern tratados en el presente captulo.
2 Conversiones de constructor
Hemos indicado ( 4.9.9) que el modelado de tipos est estrechamente relacionado con los
constructores de clases, y que la posibilidad de realizar un modelado de un objeto b de tipo B, a
otro tipo A distinto:
a = A(b);
depende de cmo est definido el constructor de la clase A. Para que esta conversin sea posible,
debe existir un constructor de conversin ( 4.11.2d1) que acepte un objeto tipo B como nico
argumento. Es decir, debe existir un mtodo:
A::A(B b) { /* detalle de la conversin */ }

En realidad, los constructores de conversin constituyen el soporte del mecanismo C++ de
modelado, de forma que la existencia de estos constructores es condicin necesaria y suficiente
para que pueda efectuarse este ltimo (el modelado). Por ejemplo:
class X {
public:
X(int); // constructor C-1
};
la mera existencia del constructor C-1 en la clase X, permite las siguientes asignaciones:
void f() {
X a = 1; // Ok. invocacin implcita a X(1)
X b(1); // Ok. invocacin implcita a X(1)
a = 2; // Ok. invocacin implcita a X(2)
a = (X) 2; // Ok. casting explcito (estlo tradicional)
a = static_cast<X>(2); // Ok. casting explcito (estilo C++)
}
Si eliminamos el constructor C-1 de la declaracin de la clase, las sentencias anteriores seran
errneas.
Nota: las tres ltimas sentencias implican en realidad dos operaciones: la creacin de un
objeto temporal tipoX conteniendo el Rvalue de la expresin y una asignacin posterior
utilizando el operador de asignacin implcito de la clase X.
2.1 Ejemplo
Para ilustrar la problemtica de este tipo de conversiones explcitas e implcitas, construiremos un
ejemplo ejecutable en el que creamos una clasePar que destinaremos a albergar los enteros
mltiplos de 2. Lo que pretendemos es poder utilizar los miembros de esta clase en todas las
circunstancias en que se podra utilizar un tipo bsico int suponiendo que su valor sea un nmero
par (divisible por 2).
El criterio para aceptar que un entero n puede ser miembro de la clase es que el resto de la
divisin n/2 sea cero (consideramos que el cero es par, puesto que 0/2 es 0).
El diseo bsico es el siguiente:
#include <iostream>
using namespace std;

class BadNumber {
int num; // miembro -privado por defecto-
public:
BadNumber(int n=0): num(n) {} // constructor explcito
int what() { return num; } // mtodo
};

class Par {
int val;
void verify(int n) { if (n % 2) throw BadNumber(n); }
public:
Par(int n=0) { // L.15: constructor de conversin
verify(n);
val = n;
cout << "Creado numero " << val << endl;
}
};

int main() { // =================
try {
Par p0; // M2: Creado numero 0
Par p2 = 2; // M3: Creado numero 2
Par p3 = 3; // M4: Error: numero 3 impar
Par p4 = (Par) 4; // M5: Creado numero 4
Par P6 = Par(6); // M6: Creado numero 6
}
catch (BadNumber& e) {
cout << "Error: numero " << e.what() << " impar." << endl;
}
}
Salida:
Las salidas producidas por las sentencias de asignacin contenidas en la funcin main, se han
incluido en los comentarios junto a las sentencias, aunque las repetimos aqu:
Creado numero 0
Creado numero 2
Error: numero 3 impar
Creado numero 4
Creado numero 6
Comentario
La clase BadNumber sirve para lanzar una excepcin en caso que se pretenda crear un
nmero Par invlido. El mtodo what nos devuelve dicho nmero.
La clase Par tiene un diseo muy simple: el miembro val almacena el nmero correspondiente. El
mtodo verify sirve para comprobar que el "valor" del objeto a crear cumple la condicin exigida
de ser par. En caso contrario lanza una excepcin ( 1.6) que ser recogida por el dispositivo try
... catch correspondiente de la funcin main.
Se ha dotado a la clase de un constructor por defecto que acepta un int como argumento. Si el
valor n suministrado pasa la verificacin correspondiente, se inicia el miembro val con el valor del
argumento n y se muestra en pantalla el nmero creado.
En los comentarios de las sentencias M2 a M6 se muestra la salida obtenida en cada caso. Puede
comprobarse que la existencia de un constructor de conversin como el definido en L.15 permite
los modelados implcitos en las sentencias M3 y M4 o explcitos (sentencias M5 y M6), donde las
constantes numricas utilizadas como Rvalues son convertidas a objetos de tipo Par.
Observe que no ha sido necesario definir el operador de asignacin = entre objetos Par. La versin
por defecto suministrada por el compilador ( 4.9.18a) resulta suficiente para las asignaciones
planteadas.
2.1 Aumentar la funcionalidad
La clase diseada cumple con su funcin de almacenar nmero pares. Tambin permite una
asignacin de tipo Par = int, y detecta cualquier intento de crear un objeto no vlido. Sin embargo,
dista mucho de poder ser utilizada con la misma generalidad que los objetos de tipo int. Por
ejemplo, las siguientes sentencias produciran un error de compilacin:
Par p3 = p2 + 2; // Error
Par p4 = p2 + p2; // Error
Par p8 = 4 + p4; // Error
La razn es que no estn definidas las operaciones correspondientes:
suma Par & int; suma Par & Par, y suma int & Par [1].
Para acercar nuestro diseo a la funcionalidad deseada agregamos los operadores
correspondientes. Seran los siguientes algoritmos:
Nota: en los ejemplos que siguen, pi es un objeto tipo Par, y n es un int.
2.1a Operador suma + entre tipos Par e int (resuelve situaciones del tipo pi + n):
operator+(int n) {
verify(n); // verificar que el operando n es adecuado
val += n;
}

2.1b Operador suma + para tipos Par (resuelve situaciones del tipo pi + pj):
Par operator+(const Par& p) {
return Par(val + p.val);
}
Observe que el valor a devolver por la funcin se ha obtenido mediante una explcita al constructor
utilizando el argumento adecuado. Aunque esta operacin no precisa de verificacin, de todas
formas el constructor la realiza.

2.1c Operador suma + entre tipos int y Par (resuelve situaciones del tipo n + pi):
Par operator+(int n, Par p) {
Par pt(n); // L1:
return pt + p; // L2:
}
La sentencia L1 crea un objeto automtico pt tipo Par y valor n, mediante una invocacin implcita
al constructor de la clase (el constructor se encarga de verificar que el valor n es correcto). En L2
se utiliza el operador suma entre objetos tipo Par, definido en el punto anterior (2.1b ), para
devolver el objeto resultante de la operacin.
Una definicin alternativa y ms eficiente, sera la siguiente:
Par operator+(int n, Par p) {
return Par(p.val + n);
}

Las dos primeras funciones-operador se integran como mtodos no estticos de clase; la ltima
como funcin externa. En el listado adjunto se muestra el diseo resultante despus de las
adiciones anteriores ( Listado-1). El nuevo diseo permite realizar las operaciones deseadas
(2.1b ) con los resultados que se indican:
Par p3 = p2 + 1; // -> Error: numero 1 impar.
Par p4 = p2 + p2; // -> Creado numero 4
Par p5 = 3 + p2; // -> Error: numero 3 impar.
Par p8 = 4 + p4; // -> Creado numero 4
// -> Creado numero 8
Observe que los resultados son los esperados; la doble salida generada por la ltima sentencia es
producida por el algoritmo 2.1c . La primera corresponde a la creacin del objeto
automtico pt. La segunda a la construccin del objeto a devolver realizada en L2.
2.2 Nuevas dificultades
No obstante lo anterior, una total libertad para la utilizacin conjunta de nuestro tipo Par con el
tipo int exigira muchas ms posibilidades. Por ejemplo:
Par p2 += 2;
Par p4 += p2;
Par p8 /= 2;
++p2;
etc.
A las anteriores habra que aadir todas las circunstancias en que la conversin deba realizarse en
sentido contrario (del tipo Par a int). Por ejemplo:
int x = p2;
int y = 3 + p2;
y += p2;
etc.
Con el diseo actual de la clase Par, todas estas sentencias producen un error de compilacin. Su
utilizacin exigira implementar toda una serie de versiones sobrecargadas de los operadores
correspondientes, lo que supone desde luego una buena cantidad de cdigo.
3 Aritmtica mixta
Los problemas encontrados para establecer una aritmtica mixta entre tipos Par e int, podran
generalizarse para cualquier par de tipos A y B. Resulta evidente que los problemas de estas
aritmticas se refieren exclusivamente a los operadores @ binarios (operaciones del tipo a @ b). Y
habida cuenta que los operadores estn implementados como funciones-operador, si hemos
establecido un lgebra para uno de los tipos (A por ejemplo), el problema de compatibilizarla con el
otro tipo se reduce a disponer de un mecanismo de conversin de tipos B A. De forma que
cuando una funcin-operador espere un argumento tipo A, el mecanismo de congruencia de
argumentos ( 4.4.1a) disponga de recursos para realizar la conversin automticamente.
Por ejemplo, en la sentencia:
int y = p2 + 3;
El compilador procede en dos fases: en la primera intenta resolver la expresin del Rvalue (p2 +
3), para a continuacin resolver la asignacin y = Rvalue.
Respecto a la primera, el compilador intentar encontrar una funcin operator+() adecuada (
4.9.18b2), que caso de existir, respondera alguno de los siguientes prototipos:
Par::operator+(int); // 3a
Par operator+(Par, int); // 3b
int operator+(Par, int); // 3c

En caso de existir alguna definicin concordante, se utilizar el mecanismo de congruencia de
argumentos para ajustar los valores de los argumentos actuales a los formales, y solo en caso de
no existir concordancia se devolver un error sealando que no existe una implementacin
adecuada de operator+ en el programa.
Nota: observe que en este caso no sirve la existencia de definiciones del tipo:
Par operator+(int, Par); // 3d
int operator+(int, Par); // 3e
y que tampoco se intentar convertir el tipo int en Par para intentar una operacin del
tipo Par + Par, o del tipo Par en int para intentar int+ int. Observe igualmente que la existencia
simultnea de las versiones 3a y 3b o 3c conducira a un error de compilacin porque ninguna
de ellas tiene preferencia y se producira ambigedad.

En el supuesto que se consiga resolver el primer paso, el compilador intentar resolver la
asignacin utilizando el mismo criterio. Observe que dependiendo de la forma
de operator+ encontrada, el resultado del Rvalue puede ser un tipo Par o int. Si se ha utilizado
la forma 3c el resultado es un int, y la asignacin int = int no presenta dificultad. Por el
contrario, cualquiera de las formas 3a o 3b produce un resultado tipo Par. La
asignacin int = Par exige que el compilador busque una funcin-miembro de la clase int que
responda al prototipo ( 4.9.18a):
int& int::operator=(Par);
En su defecto, encuentra la versin global para el tipo int:
int& int::operator=(const int&);
Su utilizacin exigira que el mecanismo de congruencia de argumentos encontrase una forma de
adaptar el argumento actual (Par) con el formal (int). Como esto no es posible, el compilador lanza
un mensaje de error: Cannot convert 'Par' to 'int'....
4 Operadores de conversin
Hemos visto que los constructores de conversin antes mencionados , permiten la existencia de
conversiones explcitas e implcitas. Sin embargo requieren un considerable esfuerzo de
programacin para establecer una aritmtica mixta entre dos tipos A y B.
La conveniencia de disponer de un sistema que permita definir fcilmente la transformacin de un
tipo B en otro A, ha motivado que el lenguaje C++ implemente la existencia de los
denominados operadores de conversin (tambin funciones de conversin); un extrao hbrido
entre los constructores y el mecanismo de sobrecarga de operadores, que utiliza las funciones-
operador [2].
Nota: Ellis y Stroustrup informan [6] que los operadores de conversin pueden realizar dos tareas
que no pueden ser realizadas por los constructores:
Definir la conversin de un tipo abstracto a un tipo simple.
Definir la conversin de una clase B a otra A sin modificar la declaracin de la clase A.
Un operador o funcin de conversin de un tipo abstracto B a otro A, es un mtodo pblico
implementado en la clase B que define una conversin del tipo B al tipo A y responde a la
siguiente declaracin:
B::operator A ();
Observe que esta declaracin se parece a la de los constructores ( 4.11.2d1) en el sentido
que no puede especificarse el valor devuelto en su declaracin (ni siguiera void), aunque la
definicin s debe contener una sentencia return. Tampoco acepta la especificacin de ningn
tipo de parmetro. Estas funciones se definen como "funcin sin aceptar argumento, devolviendo
un especificador de conversin de tipo" [3].
El componente A de la declaracin es el especificador del tipo de conversin. Se considera
parte integrante del nombre de la funcin operador, y no tiene porqu ser necesariamente un
nombre de clase. Puede ser un tipo bsico o cualquier calificador de tipo con excepcin de una
matriz o una funcin. Ejemplos:
B::operator int (); // convierte a tipo int
B::operator char* (); // convierte a tipo puntero-a-char
B::operator void* (); // convierte a tipo puntero-a-void (genrico)
B::operator bool (); // convierte a tipo bool [5]
B::operator int& (); // convierte a tipo referencia-a-int

La definicin debe incluir una sentencia return que devuelva un valor, cuyo tipo debe coincidir
con el del especificador de tipo de conversin utilizado, o ser asimilable a l mediante una
transformacin estndar. Este valor devuelto es justamente el que especifica la conversin.
Ejemplo:
class MiClase {
int val;
operator int() { return val; }
};
En cambio:
class MiClase {
int val;
operator void*() { return val; } // Error!
};
Genera un error de compilacin: Cannot convert 'int' to 'void *', ya que el
valor val devuelto no puede ser convertido a tipo void*(puntero genrico 4.2.1d).
Cuando existe un operador de este tipo, cada vez que el compilador necesite una conversin de un
objeto obj de tipo MiClase a tipo int, utilizar el valor devuelto por la invocacin (recuerde
que operator int es un solo identificador). Es decir: si existe un operador:
B::operator A () { ...; return a; }
las conversiones explcitas (mediante un modelado -cast-) o implcitas:
a = (A)b;
a = b:
son transformadas por el compilador en:
a = b.oprator A();
4.1 Ejemplo:
Retomando el caso anterior de una clase Par para albergar los enteros pares , la definicin de
un operador de conversin de los tipos Par a intpodra tener alguna de las definiciones siguientes:
operator int () { return val; } // definicin inline
Par::operator int () { return val; } // definicin offline

El listado-2 muestra el diseo resultante para la clase ( Listado-2). La mera existencia de este
mtodo en el cuerpo de Par garantiza que el compilador puede realizar automticamente todas las
conversiones de tipo Par int que sean necesarias. En todos los casos la conversin se realiza
mediante una invocacin a la funcin de conversin correspondiente:
Par::operator int()

Las sentencias que siguen muestran algunas de estas conversiones junto con los resultados
obtenidos (suponemos estas sentencias en el cuerpo de la funcin main del listado-2):
Par p2 = 2; // -> Creado numero 2
int x1 = p2 + 3; // -> x1 == 5
int x2 = p2; // -> x2 == 2
int x3 = 3 + p2; // -> x3 == 5
x3 += p2; // -> x3 == 7
cout << "X3 = " << x3 << endl;
El uso de estas conversiones no est limitada a operaciones de asignacin e
inicializacin. Tambin son posibles otras:
int x4 = (p2) ? 1 + p2 : 0; // -> x4 == 3
int x5 = (p2 && x1) ? p2 + 1 : x3; // -> x5 == 3
if (p2) cout << "Cierto"; // -> Cierto
else cout << "Falso";

4.2 Una funcin de conversin puede ser virtual ( 4.11.8a) y puede ser heredada
[4]. Ejemplo:
class B {
int x;
public: operator int () { return x; }
};
class D : public B { };
...
void foo() {
D d;
int n = d; // Ok! equivale a: n = d.B::operator int()
}

4.3 La funcin de conversin en una clase derivada no oculta la funcin de conversin de una
superclase, a menos que ambas se refieren al mismo tipo. Ejemplo:
class B {
...
public:
operator int();
};

class D : public B {
...
public:
operator char();
};
...
void func(D& d) {
if (d) { // Error!
...
}
}
La sentencia anterior produce un error de compilacin: Ambiguity between 'B::operator
int()' and 'D::operator char().

4.4 No es posible definir ms de una funcin de conversin para una clase dada. La razn de
esta limitacin es que en caso contrario se produciran ambigedades difciles de controlar.
Ejemplo:
class B {
...
public:
operator int();
operator cha();
};
...
void func(B b) {
if (b) { // Error!
...
}
}
La sentencia anterior produce un error de compilacin: Ambiguity between 'B::operator
int()' and 'B::operator char()'.


5 Reglas de uso de las conversiones de usuario
909

Vous aimerez peut-être aussi