12.3.3. Argumentos y valores de retorno

Puede parecer un poco confuso inicialmente cuando lea los ficheros OverloadingUnaryOperators.cpp, Integer.h y Byte.h y vea todas las maneras diferentes en que se pasan y devuelven los argumentos. Aunque puede 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.

  1. 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. Las operaciones aritméticas normales (como + y -, etc.) y las booleanas no cambian sus argumentos, así que lo que usará mayoritariamente es el paso por referencia const. Cuando la función es un método, esto se traduce en hacer que sea un método const. Sólo con los operadores de asignación (como +=) y operator=, que cambian el argumento de la izquierda, ese argumento no es constante, pero aun así se pasa como dirección porque se va a modificar.

  2. 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.

  3. Todos 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 que acaba de ser asignado, como (a = b).func(); para llamar a func() sobre 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 al valor izquierdo.

  4. 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 la mayoría de los compiladores de C++ soportaran el tipo predefinido 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 devolver *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ían devolverse como const o no const? Si permite que el objeto sea modificado y alguien escribe (++a).func(), func() operará sobre la propia a, pero con (a++).func(), func() opera sobre el objeto temporal devuelto por el operador postfijo operator++. Los objetos temporales son automáticamente const, así que el compilador lo señalaría, pero en favor de la coherencia puede tener más sentido hacer ambas const, como se ha 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 caso por caso.

Retorno por valor como constante

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 usa 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 hacer explícitamente const el valor de retorno o no hacerlo no tiene ningún efecto.

Sin embargo, también es posible mandar un mensaje al valor de retorno de a+b, en lugar de simplemente pasarlo a una 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.

Optimización del retorno

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 una «llamada a función de un constructor», pero no lo es. La sintaxis es la de un objeto temporal; la sentencia dice «crea un objeto Integer temporal y devué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 bastante 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 cambio, el enfoque de «devolver un objeto temporal» funciona de manera bastante diferente. Cuando el compilador ve eso, sabe que el objeto que está creando no se necesita para nada más que para devolverlo. El compilador lo aprovecha para construir el objeto directamente en la localización del valor de retorno externo a la función. Esto requiere una única llamada normal 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, aunque no cuesta nada salvo que el programador lo tenga presente, es significativamente más eficiente. Esto a menudo se llama optimización del valor de retorno.