Tabla de contenidos
Una de las características más importantes que C++ hereda de C es la eficiencia. Si la eficiencia de C++ fuese dramáticamente menor que la de C, podría haber un contingente significativo de programadores que no podrían justificar su uso.
En C, una de las maneras de preservar la eficiencia es mediante el
uso de macros, lo que permite hacer lo que parece una llamada a
una función sin la sobrecarga habitual de la llamada a función.
La macro está implementada con el preprocesador en vez del propio
compilador, y el preprocesador reemplaza todas las llamadas a
macros directamente con el código de la macro, de manera que no
hay que complicarse pasando argumentos, escribiendo código de
ensamblador para CALL
, retornando argumentos ni
implementando código ensamblador para el RETURN
. Todo el
trabajo lo realizar el preprocesador, de manera que se tiene la
coherencia y legibilidad de una llamada a una función pero sin
ningún coste.
Hay dos problemas respecto al uso del preprocesador con macros en C++. La primera también existe en C: una macro parece una llamada a función, pero no siempre actúa como tal. Esto puede acarrear dificultades para encontrar errores. El segundo problema es específico de C++: el preprocesador no tiene permisos para acceder a la información de los miembros de una clase. Esto significa que las macros de preprocesador no pueden usarse como métodos de una clase.
Para mantener la eficiencia del uso del preprocesador con macros
pero añadiendo la seguridad y la semántica de ámbito de verdaderas
funciones en las clases. C++ tiene las funciones
inline
. En este capítulo veremos los problemas del uso de
las maros de preprocesador en C++, cómo se resuelven estos
problemas con funciones inline
, y las directrices e
incursiones en la forma en que trabajan las
funciones inline.
La clave de los problemas con las macros de preprocesador radica en que puedes caer en el error de pensar que el comportamiento del preprocesador es igual que el del compilador. Por supuesto, la intención era que una macro se parezca y actúe como una llamada a una función, por eso es bastante fácil caer en este error. Las dificultades comienzan cuando las diferencias aparecen subyacentes.
Consideremos un ejemplo sencillo:
#define F (x) (x + 1)
Ahora, si hacemos una llamada a F
de esta
manera:
F(1)
El preprocesador la expande de manera inesperada:
(x) (x + 1)(1)
El problema se debe al espacio entre `F` y su paréntesis de apertura en la definición de la macro. Cuando el espacio es eliminado en el código de la macro, puedes llamar a la función incluso incluyendo el espacio.
F (1)
Y se expandirá de manera correcta a lo siguiente:
(1 + 1)
El ejemplo anterior es un poco trivial y el problema es demasiado evidente. Las dificultades reales ocurren cuando se usan expresiones como argumentos en llamadas a macros.
Hay dos problemas. El primero es que las expresiones pueden expandirse dentro de la macro de modo que la precedencia de la evaluación es diferente a lo que cabría esperar. Por ejemplo:
#define FLOOR(x,b) x>=b?0:1
Ahora, si usamos expresiones como argumentos:
if (FLOOR(a&0x0f,0x07)) // ...
La macro se expandiría a:
if (a&0x0f>=0x07?0:1)
La precedencia del & es menor que la del >=, de modo que la evaluación de la macro te sorprenderá. Una vez hayas descubierto el problema, puedes solucionarlo insertando paréntesis a todo lo que hay dentro de la definición de la macro. (Este es un buen método a seguir cuando defina macros de preprocesador), algo como:
#define FLOOR(x,b) ((x)>=(b)?0:1)
De cualquier manera, descubrir el problema puede ser difícil, y no dará con él hasta después de haber dado por sentado el comportamiento de la macro en sí misma. En la versión sin paréntesis de la macro anterior, la mayoría de las expresiones van a actuar de manera correcta a causa de la precedencia de >=, que es menor que la mayoría de los operadores como +, /, --, e incluso los operadores de desplazamiento. Por lo que puede pensar que funciona con todas las expresiones, incluyendo aquellas que empleen operadores lógicos a nivel de bit.
El problema anterior puede solucionarse programando cuidadosamente: poner entre paréntesis todo lo que esté definido dentro de una macro. De todos modos el segundo problema es más sutil. Al contrario de una función normal, cada vez que usa argumentos en una macro, dicho argumento es evaluado. Mientras la macro sea llamada solo con variables corrientes, esta evaluación es benigna, pero si la evaluación de un argumento tiene efectos secundarios, entonces los resultados pueden ser inesperados y definitivamente no imitaran el comportamiento de una función.
Por ejemplo, esta macro determina si un argumento entra dentro de cierto rango:
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
Mientras use un argumento «ordinario» la macro trabajará de manera bastante similar a una función real. Pero en cuanto se relaje y comience a creer que realmente es una función, comenzarán los problemas. Así:
//: C09:MacroSideEffects.cpp #include "../require.h" #include <fstream> using namespace std; #define BAND(x) (((x)>5 && (x)<10) ? (x) : 0) int main() { ofstream out("macro.out"); assure(out, "macro.out"); for(int i = 4; i < 11; i++) { int a = i; out << "a = " << a << endl << '\t'; out << "BAND(++a)=" << BAND(++a) << endl; out << "\t a = " << a << endl; } } ///:~
Listado 9.1. C09/MacroSideEffects.cpp
Observe el uso de caracteres en mayúscula en el nombre de la macro. Este es un buen recurso ya que advierte al lector que esto es una macro y no una función, entonces si hay algún problema, actúa como recordatorio.
A continuación se muestra la salida producida por el programa, que no es para nada lo que se esperaría de una auténtica función:
a = 4 BAND(++a)=0 a = 5 a = 5 BAND(++a)=8 a = 8 a = 6 BAND(++a)=9 a = 9 a = 7 BAND(++a)=10 a = 10 a = 8 BAND(++a)=0 a = 10 a = 9 BAND(++a)=0 a = 11 a = 10 BAND(++a)=0 a = 12
Cuando a
es cuatro, sólo ocurre la primera
parte de la condición, de modo que la expresión es evaluada sólo
una vez, y el efecto resultante de la llamada a la macro es que
a
será 5, que es lo que se esperaría de una
llamada a función normal en la misma situación. De todos modos,
cuando el número está dentro del rango, se evalúan ambas
condiciones, lo que da como resultado un tercer incremento. Una
vez que el número se sale del rango, ambas condiciones siguen
siendo evaluadas de manera que se obtienen dos incrementos. Los
efectos colaterales son distintos, dependiendo del argumento.
Este no es desde luego el comportamiento que se quiere de una
macro que se parece a una llamada a función. En este caso, la
solución obviamente es hacer una autentica función, lo que de
hecho implica la cabecera extra y puede reducir la eficiencia si
se llama demasiado a esa función. Desafortunadamente, el
problema no siempre será tan obvio, y sin saberlo. puede estar
utilizando una librería que contiene funciones y macros juntas,
de modo que un problema como éste puede esconder errores
difíciles de encontrar. Por ejemplo, la macro
putc()
de cstdio
puede
llegar a evaluar dos veces su segundo argumento. Esto está
especificado en el Estándar C. Además, la implementación
descuidada de toupper()
como una macro
puede llegar a evaluar el argumento más de una vez, lo que dará
resultados inesperados con
toupper(*p++)
[66].
Por supuesto, C requiere codificación cuidadosa y el uso de macros de preprocesador, y se podría hacer lo mismo en C++ si no fuese por un problema: las macros no poseen el concepto de ámbito requerido con los métodos. El preprocesador simplemente hace substitución de texto, de modo que no puede hacer algo como:
class X{ int i; public: #define VAL(X::i) // Error
ni nada parecido. Además, no habría ninguna indicación del
objeto al que se está refiriendo. Simplemente no hay ninguna
forma de expresar el ámbito de clase en una macro. No habiendo
ninguna alternativa diferente a macros de preprocesador, los
programadores se sentirán tentados de crear algunos atributos
públicos por el bien de la eficiencia, exponiendo así la
implementación subyacente e impidiendo cambios en esa
implementación, así como eliminando la protección que
proporciona private
.