9: Funciones inline

Tabla de contenidos

9.1. Los peligros del preprocesador
9.2. Funciones inline
9.3. Stash y Stack con funciones inline
9.4. Funciones inline y el compilador
9.5. Reducir el desorden
9.6. Más características del preprocesador
9.7. Comprobación de errores mejorada
9.8. Resumen
9.9. Ejercicios

Una de las características más importantes que C++ hereda de C es la eficiencia. Si la eficiencia de C++ fuese drásticamente menor que la de C, habría 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 ningún coste por apilar argumentos, hacer un CALL en ensamblador, retornar argumentos ni ejecutar un RETURN en ensamblador. Todo el trabajo lo realiza el preprocesador, de manera que se tiene la comodidad 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++. El primero también existe en C: una macro parece una llamada a función, pero no siempre actúa como tal. Esto puede ocultar errores difíciles de encontrar. El segundo problema es específico de C++: el preprocesador no tiene permisos para acceder a los atributos 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 se verán los problemas del uso de las macros de preprocesador en C++, cómo se resuelven estos problemas con funciones inline, y algunas directrices y claves sobre la forma en que trabajan las funciones inline.

9.1. Los peligros del preprocesador

La clave de los problemas con las macros de preprocesador radica en que puede 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 pareciera y actuara como una llamada a una función, por eso es bastante fácil caer en este error. Las dificultades comienzan cuando aparecen las diferencias sutiles.

Considere un ejemplo sencillo:

#define F (x) (x + 1)

Ahora, si se hace una llamada a F de esta manera:

F(1)

El preprocesador la expande, de manera algo inesperada, así:

(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 se elimina en la definición de la macro, puede llamar a la macro incluso dejando el espacio.

F (1)

Y se expandirá de manera correcta a lo siguiente:

(1 + 1)

El ejemplo anterior es bastante trivial y el problema se hará evidente enseguida. 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 se usan 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 le sorprenderá. Una vez haya descubierto el problema, puede solucionarlo poniendo entre paréntesis todo lo que hay dentro de la definición de la macro. (Es una buena práctica cuando se definen macros de preprocesador). Así:

#define FLOOR(x,b) ((x)>=(b)?0:1)

Sin embargo, descubrir el problema puede ser difícil, y puede que no dé con él hasta después de haber dado por sentado que la macro se comporta correctamente. 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 un argumento en una macro, dicho argumento es evaluado. Mientras la macro sea llamada sólo con variables corrientes, esta evaluación es benigna, pero si la evaluación de un argumento tiene efectos laterales, entonces los resultados pueden ser inesperados y definitivamente no imitarán 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. Éste es un buen recurso ya que advierte al lector de que esto es una macro y no una función, de modo que, si hay algún problema, actúa como un pequeño 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 laterales son distintos, dependiendo del argumento.

Éste 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 obvia es hacer una auténtica función, lo que por supuesto añade la sobrecarga extra y puede reducir la eficiencia si se llama mucho 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++)[67].

9.1.1. Macros y acceso

Por supuesto, en C hay que programar con cuidado y usar con cuidado las macros de preprocesador, y sin duda 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. Sin alguna alternativa a las 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.



[67] Andrew Koenig entra en más detalles en su libro C Traps & Pitfalls (Addison-Wesley, 1989).