Puede parecer un poco confuso inicialmente cuando lea los archivos
OverloadingUnaryOperators.cpp
, Integer.h
y Byte.h
y vea todas las maneras diferentes en que se pasan y
devuelven los argumentos. Aunque usted pueda pasar y devolver argumentos de la
forma que prefiera, las decisiones en estos ejemplos no se han realizado al
azar. Siguen un patrón lógico, el mismo que querrá usar en la mayoría de sus
decisiones.
Como con cualquier argumento de función, si sólo necesita leer el argumento y
no cambiarlo, lo usual es pasarlo como una referencia
const
. Normalmente operaciones aritméticas (como +
y
-
, etc.) y booleanas no cambiarán sus argumentos, así que
pasarlas como una referencia const
es lo que veré
mayoritariamente. Cuando la función es un método, esto se traduce en una
método const
. Sólo con los operadores de asignación (como
+=
) y operator=
, que cambian el argumento de
la parte derecha, no es el argumento derecho una constante, pero todavía se
pasa en dirección porque será cambiado.
El tipo de valor de retorno que debe seleccionar depende del significado
esperado del operador. (Otra vez, puede hacer cualquier cosa que desee con los
argumentos y con los valores de retorno). Si el efecto del operador es
producir un nuevo valor, necesitará generar un nuevo objeto como el valor de
retorno. Por ejemplo, Integer::operator+
debe producir un
objeto Integer
que es la suma de los operandos. Este
objeto se devuelve por valor como una constante así que el resultado no se
puede modificar como un «valor izquierdo».
Todas los operadores de asignación modifican el valor izquierdo. Para permitir
que el resultado de la asignación pueda ser usado en expresiones encadenadas,
como a = b = c
, se espera que devuelva una referencia al mismo
valor izquierdo que acaba de ser modificado. Pero ¿debería ser esta referencia
const
o no const
?. Aunque lea a = b = c
de
izquierda a derecha, el compilador la analiza de derecha a izquierda, así que
no está obligado a devolver una referencia no const
para soportar
asignaciones encadenadas. Sin embargo, la gente a veces espera ser capaz de
realizar una operación sobre el elemento de acaba de ser asignado, como
(a = b).func();
para llamar a func
de
a
después de asignarle b
. De ese modo,
el valor de retorno para todos los operadores de asignación debería ser una
referencia no const
para el valor izquierdo.
Para los operadores lógicos, todo el mundo espera obtener en el peor de los
casos un tipo int
, y en el mejor un tipo bool
. (Las
librerías desarrolladas antes de que los compiladores de C++ soportaran el
tipo incorporado bool
usaban un tipo int
o un
typedef
equivalente).
Los operadores de incremento y decremento presentan un dilema a causa de las
versiones postfija y prefija. Ambas versiones cambian el objeto y por tanto no
pueden tratar el objeto como un const
. La versión prefija devuelve el
valor del objeto después de cambiarlo, así que usted espera recuperar el objeto
que fue cambiado. De este modo, con la versión prefija puede simplemente revolver
*this
como una referencia. Se supone que la versión postfija
devolverá el valor antes de que sea cambiado, luego está forzado a crear un objeto
separado para representar el valor y devolverlo. Así que con la versión postfija
debe devolverlo por valor si quiere mantener el significado esperado. (Advierta
que a veces encontrará operadores de incremento y decremento que devuelven un
int
o un bool
para indicar, por ejemplo, que un objeto
preparado para moverse a través de una lista está al final de ella). Ahora la
pregunta es: ¿Debería éste devolverse como una referencia const
o no
const
?. Si permite que el objeto sea modificado y alguien escribe
(a++).func()
, func
operará en la propia
a
, pero con (++a).func()
,
func
opera en el objeto temporal devuelto por el operador
postfijo operator++
. Los objetos temporales son automáticamente
const
, así que esto podría ser rechazado por el compilador, pero en favor
de la consistencia tendría más sentido hacerlos ambos const
como hemos
hecho aquí. O puede elegir hacer la versión prefija no const
y la postfija
const
. Debido a la variedad de significados que puede darle a los
operadores de incremento y decremento, deben considerarse en términos del caso
individual.
El retorno por valor como una constante puede parecer un poco sutil al
principio, así que es digno de un poco más de explicación. Considere el operador
binario operator+
. Si lo ve en una expresión como
f(a+b)
, el resultado de a+b
se convierte en un objeto
temporal que se usará en la llamada a f()
. Debido a que es
temporal, es automáticamente const
, así que aunque, de forma
explicita, haga el valor de retorno const
o no, no tendrá efecto.
Sin embargo, también es posible mandar un mensaje al valor de retorno de
a+b
, mejor que simplemente pasarlo a la función. Por ejemplo, puede
decir (a+b).g()
en la que g()
es algún método
de Integer
, en este caso. Haciendo el valor de retorno
const
, está indicando que sólo un método const
puede ser
llamado sobre ese valor de retorno. Esto es correcto desde el punto de vista del
const
, porque le evita almacenar información potencialmente importante
en un objeto que probablemente será destruido.
Advierta la manera que se usa cuando se crean nuevos objetos para ser devueltos
por valor. En operator+
, por ejemplo:
return Integer(left.i + right.i);
Esto puede parecer en principio como una «función de llamada a un
constructor» pero no lo es. La sintaxis es la de un objeto temporal; la
sentencia dice «crea un objeto Integer
temporal y
desvuélvelo». A causa de esto, puede pensar que el resultado es el mismo
que crear un objeto local con nombre y devolverlo. Sin embargo, es algo
diferente. Si en su lugar escribiera:
Integer tmp(left.i + right.i); return tmp;
sucederían tres cosas. La primera, se crea el objeto tmp
incluyendo la llamada a su constructor. La segunda, el constructor de copia
duplica tmp
en la localización del valor de retorno
externo. La tercera, se llama al destructor para tmp
cuando
sale del ámbito.
En contraste, la aproximación de «devolver un objeto temporal» funciona de manera bastante diferente. Cuando el compilador ve eso, sabe que no tiene otra razón para crearlo mas que para devolverlo. El compilador aprovecha la ventaja que ofrece para construir el objeto directamente en la localización del valor de retorno externo a la función. Esto necesita de una sola y ordinaria llamada al constructor (la llamada al constructor de copia no es necesaria) y no hay llamadas al destructor porque nunca se crea un objeto local. De esta manera, no requiere nada más que el conocimiento del programador, y es significativamente mas eficiente. Esto a menudo se llama optimización del valor de retorno.