4.9.18b3 Sobrecarga de operadores de manejo de bits
§1 Sinopsis
En el presente epígrafe trataremos la sobrecarga de los operadores de manejo de bits ("Bitwise"). En concreto, los operadores de desplazamiento derecho >> e izquierdo <<.
Recordemos (
4.9.3) que son operadores binarios que responden a la sintaxis:
expr-desplazada >> expr-desplazamiento
expr-desplazada << expr-desplazamiento
En la versión global (existente en el leguaje para los tipos básicos), ambos operandos deben ser de tipo entero, y el patrón de bits representado por expr-desplazada, sufre un desplazamiento, derecho o izquierdo, acorde con el valor indicado en expr-desplazamiento, que una vez promovida a entero, debe ser un entero positivo y menor que la longitud del primer operando.
En nuestro caso, recurriremos a sobrecargarlos para una clase Char,
cuyos objetos están destinados a contener los caracteres del alfabeto.
Para simplificar, supondremos que son las mayúsculas del juego de caracteres US-ASCII
( 2.2.1a).
Son 26 caracteres, de la 'A' a la 'Z', a lo que corresponden los valores
decimales 65 a 90 inclusive. El esqueleto de nuestra clase tiene el siguiente diseño:
class Char {
public: char letra;
}
El comportamiento que queremos para los operadores << y >>
en nuestra clase tiene dos vertientes: en la primera, dado un objeto obje
tipo Char que almacene una letra. Por ejemplo, obje.letra == 'E',
el resultado de aplicarle el desplazamiento derecho o izquierdo de valor n,
supondría cambiar el valor de char según un desplazamiento equivalente en el abecedario. Por ejemplo:
obje >> 2 // -> obje.letra == 'G'
obje << 2 // -> obje.letra == 'C'
// etc.
En la segunda forma, si el segundo operando es otro objeto tipo Char, se considerará que equivale al desplazamiento correspondiente a la posición de la letra en la secuencia. Por ejemplo, en el caso anterior, si objc es otro objeto tipo Char tal que objc.letra == 'C' (tercera letra de la secuencia), se tendría:
obje >> objc // -> obje.letra == 'H'
obje << objc // -> obje.letra == 'B'
Puesto que se trata de operadores binarios, la sobrecarga puede hacerse de dos formas:
a. Declarando una función miembro no estática que acepte un argumento: x.operator@(y)
b. Declarando una función no miembro que acepte dos argumentos: operator@(x, y)
En nuestro caso utilizaremos la versión a, y es evidente que debemos utilizar dos versiones de cada operador: la primera aceptará un int como argumento. La segunda aceptará un objeto tipo Char. Además, puesto que deseamos poder utilizar nuestros operadores en expresiones encadenadas del tipo
obj1 << obj2 >> obje3 << obj4;
está claro que debemos utilizar referencias como valor devuelto (véase al respecto la discusión relativa al operador suma
binaria +
4.9.18b). El resultado de incluir la definición de los cuatro operadores es el siguiente
diseño:
class Char { // clase para caracteres ASCII
public: char caracter;
Char& operator>> (int desp) {
int pos = caracter; // valor ACII
caracter = static_cast<char> (pos + desp);
return *this;
}
Char& operator>> (Char& ch) {
int pos = caracter;
int desp = ch.caracter - 64; // posicion en la secuencia
caracter = static_cast<char> (pos + desp);
return *this;
}
Char& operator<< (int desp) {
int pos = caracter;
caracter = static_cast<char> (pos - desp);
return *this;
}
Char& operator<< (Char& ch) {
int pos = caracter;
int desp = ch.caracter - 64;
caracter = static_cast<char> (pos - desp);
return *this;
}
};
Comentario
Los dos primeros métodos representan las dos versiones del operador desplazamiento derecha >>, que se usarán respectivamente según el segundo operador sea un int o un Char. Los otros dos son simétricos de los anteriores, y corresponden al operador de desplazamiento izquierda <<. Observe que en todos los casos se devuelve una referencia al propio objeto, una vez modificado, mediante la indirección del puntero this.
La sentencia que calcula el valor ASCII del objeto antes de la modificación:
int pos = caracter; // valor ACII
implica una conversión estándar de tipo char → int, que es realizada automáticamente por el compilador. En cambio, la sentencias que calculan el carácter final:
caracter = static_cast<char> (pos + desp);
caracter = static_cast<char> (pos - desp);
utilizan una conversión explícita de tipos (
4.9.9b) int → char, que resulta más ortodoxa.
En el caso de los operadores globales, el Estándar establece que si expr-desplazamiento resulta en un valor mayor
que la longitud del primer operando, el resultado es indefinido (depende de la implementación). En nuestra versión, debemos
incluir salvaguardas para el caso que el valor ASCII resultante correspondiera a un carácter fuera de la secuencia (menor que la
'A' o mayor que la 'Z'). Esto puede hacerse controlando los resultados (pos + desp) y (pos - desp) mediante una
función que tome las medidas correctoras pertinentes. Por ejemplo:
int control (int resultado) {
while (resultado > 90 ) { resultado -= 26; }
while (resultado < 65 ) { resultado += 26; }
return resultado;
}
Esta función define un desplazamiento circular; cuando a un carácter le corresponde un valor inmediatamente posterior al máximo 'Z', vuelve a corresponderle el primero ('A'). Del mismo modo, cuando a un carácter le corresponde el valor anterior al mínimo 'A', vuelve a corresponderle el máximo ('Z'). Esta función la implementamos como un método privado en la clase [1]. Además, incluimos en cada función-operador un par de sentencias que nos muestren los valores del carácter antes y después de la transformación producida por el operador (estos controles nos permitirán más adelante comprobar el orden de ejecución en el caso de expresiones encadenadas).
Una vez incluidas las modificaciones anteriores, el diseño de la clase queda como sigue:
class Char { // clase para caracteres ASCII
public: char caracter;
Char& operator>> (int desp) {
int pos = caracter; // valor ACII
char previo = caracter;
caracter = static_cast<char> (control(pos + desp));
cout << "Caracter " << previo << " -> " << caracter << endl;
return *this;
}
Char& operator>> (Char& ch) {
int pos = caracter;
int desp = ch.caracter - 64; // posicion en la secuencia
char previo = caracter;
caracter = static_cast<char> (control(pos + desp));
cout << "caracter " << previo << " -> " << caracter << endl;
return *this;
}
Char& operator<< (int desp) {
int pos = caracter;
char previo = caracter;
caracter = static_cast<char> (control(pos - desp));
cout << "Caracter " << previo << " -< " << caracter << endl;
return *this;
}
Char& operator<< (Char& ch) {
int pos = caracter;
int desp = ch.caracter - 64;
char previo = caracter;
caracter = static_cast<char> (control(pos - desp));
cout << "caracter " << previo << " -< " << caracter << endl;
return *this;
}
private: int control (int);
};
int Char::control (int resultado) {
while (resultado > 90 ) { resultado -= 26; }
while (resultado < 65 ) { resultado += 26; }
return resultado;
}
Comentario
El único punto a destacar es que hemos tenido que almacenar un valor auxiliar previo, con el estado anterior a la modificación para poder mostrar los valores anterior y posterior a la acción del operador. También que las expresiones que calculan el carácter final hemos sustituido los valores (pos + desp) y (pos - desp) por el resultado de la invocación al método control.
Con esta definición para Char, y suponiendo que, en todos los casos los valores de partida son:
Char a = { 'A' };
Char e = { 'E' };
Char c = { 'C' };
Char z = { 'Z' };
Se obtienen los resultados siguientes:
a >> 27; // Caracter A -> B
z << 27; // Caracter Z -< Y
e >> 2; // Caracter E -> G
e << 5; // Caracter E -< Z
e >> c; // caracter E -> H
e << c; // caracter E -< B
A continuación probamos algunas expresiones encadenadas:
e >> c >> 2; // caracter E -> H \n Caracter H -> J
e >> c >> c; // caracter E -> H \n caracter H -> K
e >> c << a; // caracter E -> H \n caracter H -< G
Recordemos que estos operadores tienen efectos laterales, en el sentido de que modifican el operando a la izquierda (en su caso, el derecho no sufre modificación). Como puede verse, el orden de evaluación es de izquierda a derecha, de forma, que las expresiones anteriores equivalen a:
((e >> c) >> 2);
((e >> c) >> c);
((e >> c) << a);
[1] Este mátodo lo definimos off-line porque el compilador
Borland nos avisa que: Functions containing while are not expanded inline
.
Es decir, las funciones que contengan bucles while no son suceptibles de expansión
in-line, con lo que no tiene sentido mantener su definición dentro del cuerpo de la clase.