You are currently browsing the tag archive for the ‘C++’ tag.

Punteros a funciones son variables que guardan una dirección de memoria: la dirección de una función. Punteros son una especie de referencia. En muchos lenguajes de programación existe el concepto de referencias a funciones que se pueden guardar en una variable. En este artículo hablamos de punteros de funciones como se usan en los lenguajes C y C++. Sin embargo, los conceptos se dejan aplicar a cualquier tipo de referencias a funciones – también a aquella otra que usa C++: los functores.

El caso clásico de usar punteros a funciones es una cola de eventos. Si programas una cola de eventos, entonces no sabes cómo se llamarán las funciones (manejadores de eventos) que se registrarán. Por eso no puedes llamarles por nombre. De hecho, colas de eventos se implementan frecuentemente en una biblioteca cuyo desarrollo es independiente de la programación de las funciones a registrar. Pues, no sabrás los nombres de las funciones y tampoco necesitas saberlos.

Tu cola de eventos suministras una función de registro. Esta función tiene un parámetro cuyo tipo es una función. Es decir, aunque no sabes los nombres de las funciones, sí, obligas a que tengan un tipo conocido: el que defines tú.

Tu cola de eventos tiene una lista con punteros a funciones que tienen este tipo. Por este punto de vista, una cola de evento no es muy diferente a una base de datos, donde el usuario puede guardar (es decir, registrar) datos. La differencia es que a los datos de una cola de eventos se puede llamar como función.

La ventaja de todo esto: Puedes llamar a un número arbitrario de funciones sin necesitar la declaración de estas funciones de antemano.

No obstante, esto lleva también una pequeña desventaja. Herramientas de análisis de código pueden extraer del código, qué función llama a qué otra función. Esto es una inforamción útil cuando quieres saber a qué «clientes» afectas al cambiar una función. No obstante, este análisis no funciona con punteros a funciones, ya que el valor de ellos sólo se sabrá a tiempo de ejecución. Sin embargo, esto no suele ser una restricción importante. Las funciones que se llaman mediante puntero suelen ser pocas y bien claras. Por ejemplo, una función de nombre OnMouseClick es fácil de reconocer como manenjador de eventos. Además, no es buen estilo de llamar a manejadores de eventos por nombre desde otras funciones. Así no lo harías, ¿verdad?

Muchas colas de eventos suelen guardar más datos que el puntero a la función. La razón es que no quieres escribir 20 manejadores para eventos que sólo se diferencian por un parámetro. La función de registro te permite añadir un dato de tipo identificador, que pasa como parámetro al manejador de evento. El manejador de evento es, por lo tanto, un tipo de función que lleva este parámetro. Dentro del manejador puedes leer este dato y saber así, a base de qué registro fue llamado. Así puedes registrar la misma función para varios eventos.

La pregunta es: ¿qué es el tipo más adecuado para este dato identificador? Pues, muchas implementaciones usan un tipo «cualquier cosa». En Java, esto sería un Object, en C y C++ gusta un puntero de tipo void* con un parámetro adicional con el tamaño del buffer a que el puntero apunta. De esta forma puedes registrar cualquier dato en la cola de eventos, sin que la cola necesita conocer su estructura. El tipo real sabrá el manejador de eventos, que convierte el tipo «cualquier cosa» en el tipo que realmente espera.

// Al registrar el manejador usas un identificador de tipo struct Id
struct Id
{
    int index;
    char subindex;
};
Id mi_id = { 5, 1 };
cola_de_eventos.registra(mi_callback, mi_id);

// Tu manejador de eventos haría esto
void mi_callback(const void*const datos)
{
    // Tu manejador sabe que está por detrás de estos "datos"
    const Id*const id = static_cast<const Id*const>(datos);
    
    // Aquí haría algo con estos datos como
    // switch (id.index) ...
}

Ten en cuenta que los datos mencionados arriba se guardan junto con la dirección a la función de antemano. Pues, identifican el registro. No es lo mismo que parámetros que se producen junto con el evento. Por ejemplo, un manejador de OnMouseClick esperaría las coordenadas del ratón, donde tuvo lugar el click. Cómo diferentes eventos pueden producir diferentes datos subyacentes, se encuentra a menudo la idea de usar un tipo «cualquier cosa» también para estos parámetros generados en el evento.

Cuando creas un registro de funciones como una cola de eventos, te deberías preguntar, qué tipo de datos quieres permitir. Un tipo «cualquier cosa» da mucha flexibilidad – también para su abuso. Y no es necessario si sólo quieres permitir un identificador.

Si prefieres usar clases en lugar de punteros, define quién debe hacer la limpieza en la memoria. Pasar un objeto por referencia obliga a guardarlo hasta que ya no se usa. Pasar un objeto por copia deja claro al autor del manejador que se debe hacer una copia antes de terminar el manejador si no quiere perder los datos.

Piensa también que C++ permite functores. Functores son clases que sobrecargan el operador (). Así se puede tratar una instancia de esta clase como función: mi_instancia() llama realmente a mi_instancia.operator(). Functores son instancias de clases y estas se protegen mejor contra un fraude como lo permitirían los punteros. ¡Imagínate que el autor de la cola de eventos es malo y no llama al puntero de función que registraste sino a esta dirección más cuatro bytes!

Como ves, punteros a funciones son muy útiles para determinadas tareas como cola de eventos. Puede tener sentido guardar datos de identificación junto con el puntero y puede ser útil manejar un tipo «cualquier cosa» para pasar datos de tipo arbitrario. No obstante es importante de pensar bien, si es mejor restringir la flexibilidad.

Lectura adicional

¿Qué son functores?

El lenguaje C ofrece punteros a funciones. Estos permiten llamar a diferentes funciones según el valor del puntero. Un puntero así no apunta a un dato sino a un comportamiento – el comportamiento que implementa la función.

Punteros ofrecen, además, apuntar a direcciones de memorias erróneas. Así es posible ejecutar cualquier dato como función. Esto encanta a los autores de código malicioso. Para los demás interesa evitarlo, es decir, como se puede limitar las direcciones a que un puntero de función puede apuntar.

La solución viene con C++. Functores en el lenguaje C++ son clases que tienen sobrecargado el operador(). Esto permite utilizar una instancia de una clase como una función – y sólo una función y no cualquiera.

¿Cómo se usan functores?

El functor más simple es una clase como en este ejemplo:

class MiFunctor
{
public:
    int operator() ()  { return 1; }
};

Este functor es una clase con el método operator() sin parámetros. Nótese que la primera pareja de paréntesis es el nombre del operador y la segunda para los parámetros si los hubiera.

La clase de functor de arriba se puede usar como en el ejemplo siguiente.

MiFunctor miInstancia;
std::cout << miInstancia();

La primera línea crea un objeto del functor y la segunda llama primero al método MiFunctor::operator() y luego imprime el valor de retorno – que es un uno.

Dónde se usan los functores

El método operator() puede tener también parámetros. Como ejemplo definimos un functor que devuelve el menor de dos vectores.

// El tipo de vector que usamos
struct Vector
{
    float x, y;
};

// El functor que compara dos vectores
class VectorMenor
{
public:
    bool operator() (const Vector& a, const Vector& b) const
    {
        // La forma de comparación no importa aquí.
        // Pero para ser completo lo especificamos:
        // devuele a, si la primera coordenada de a es
        // menor o si la primera es igual y la segunda
        // es menor.
        if (a.x < b.x || (a.x == b.x && a.y < b.y))
        {
            return a;
        }
        else
        {
            return b;
        }
    }
};

// Y así usamos el functor
Vector u, v;
bool u_es_menor = VectorMenor(u, v);

Pues bien, matemáticamente no hay una operación menor entre dos vectores. Así ¿para qué sería útil este functor VectorMenor?

Echemos un vistazo a la definición (simplificada) del contendor std::map de la biblioteca estándar de C++.

template <class Key,
          class Value,
          class Compare = std::less<Key> >
class std::map {};

¿Una vez te has preguntado qué signífica este std::less?

Pues, la clase std::map guarda sus elementos ordenados por el atributo – es decir la clase Key. Así no necesita buscar por todos los elementos cuando busca uno en concreto. Comprueba si el atributo del elemento buscado es mayor or menor que el atributo del elemento en medio de todos los elementos. Según el resultado continúa la búsqueda con la mitad inferior o superior. De esta manera se divide entre dos el rango de elementos a buscar tras cada comparación. Esto es mucho más rápido que buscar por todos los elementos.

Para ordenar los elementos por los atributos, la clase std::map require que se pueda aplicar el operador < al tipo del atributo. Este operador existe para los tipos básicos pero no para una clase cualquiera. Por eso, la clase std::map ofrece especificar un functor que determina la «menor» de dos instancias de Key. El método operator() de este functor toma dos argumentos de tipo Key como nuestro ejemplo VectorMenor. El functor de comparación por defecto es std::less – que simplemente usa el operator< global.

Un mapa que guarda cadena de textos ordenados por vectores, se puede definir así:

std::map<Vector, std::string, VectorMenor> mi_mapa;

Nótese que el functor no es una variable que apunta a una función sino un tipo de una variable. Es decir, una clase.

Herencia de functores

Los functores son clases de C++ y, por lo tanto, se pueden definir todos los métodos en ellos como en cualquier otra clase. Y también se pueden heredar.

Un functor derivado puede sobrescribir el método operator() con su propia implementatción. Más interesante es aún declarar un operator() virtual. Así podemos crear una interfaz de functor que se puede implementar de varias formas.

Como ejemplo definimos una interfaz para comparar cadenas de textos en diferentes idiomas. Esto puede ser interesante porque el orden alfabético varia – en español incluso en el mismo idioma: antiguamente se consideró la combinación «ll» como una letra que se ordenó tras la «l» simple. Por eso, la palabra «llamar» vino después de «luna» en los diccionarios antiguos.

// Definimos una interfaz para comparar cadenas de textos.
class InterfazCadenaDeTextoMenor
{
public:
    virtual bool operator() (const std::string& a,
                             const std::string& b) const = 0;
};

La interfaz obliga que el método operator() tenga dos parámetros del tipo std::string y devuelve un bool. El método operator() está declarado const ya que no modifica variables internos de las clase InterfazCadenaDeTextoMenor. No obstante, esto no es obligatorio. Puede haber functores con memoria que, sí, modifican datos internos.

Es importante tener la misma signatura cuando trabajamos con métodos virtuales. Por eso los tipos de retorno y de parámetros deben estar definidos fuera de la clase. Si usaramos InterfazCadenaDeTextoMenor en la clase base como tipo parámetro y ClaseDerivada en la clase derivada, entonces el método virtual no sería sobrescrito, ya que tendría otra signatura.

Boost Bind and std::function

Los punteros de funciones y los functores pueden aparecer igual en el uso.

// Declaración de tipo de un functor
class Functor
{
public:
    void operator() ();
};

// Declaración de tipo de un puntero de función
typedef void (*Puntero)();

// Instancia de un functor y un puntero
Functor functor;
Puntero puntero;

// Uso de un functor y un puntero
functor();
puntero();

Como vemos, tanto el functor como el puntero aparecen con nada más que paréntesis. Para C++ son dos tipos muy distintos, pero para el usuario hacen lo mismo. Por eso vino la idea de crear un super-tipo que une los punteros y los functores en un comportamiento igual. Este existe en la biblioteca Boost con en elemento boost::bind.

Con boost::bind podemos crear un tipo que se inicializa o bien con un puntero de función o bien con un functor. Algo similar es el objeto std::function de la biblioteca de C++ a partir del estándar 2011.

Conclusión

Los functores tienen el mismo uso como punteros de funciones. No obstante ofrecen la seguridad de no ejecutar código cualquiera como puede pasar con punteros mal inicializados. Además, los functores ofrecen todas las capabilidades de clases de C++. En código mixto de punteros y functores se ofrece boost::bind y std::function como tipo común.

Referencias

Lectura adicional

Punteros a funciones son una herramienta potente para agilizar el flujo de un programa. ¡Y una forma eficaz de perderse en el código!

El concepto de punteros o «referencias» a funciones existe en muchos lenguajes de programación. Por ejemplo, en Java Script, se puede asignar una función a una variable miembro de un objeto. Esta variable representa entonces la función asignada. En C y C++, estas variables deben tener, además, el tipo de la función a que apuntan. Restringir el tipo de la función reduce el número de posibles errores a la hora de ejecución y los incrementa a la hora de compilar el código.

En este artículo nos enfocamos a punteros de funciones en C y C++ y asumimos que el lector ya está familiarizado con las diferentes opciones de declarar const a variables y punteros, como se describen en el artículo
Como usar const en C++
.

La declaración de un tipo de función

Comenzemos con un ejemplo. ¿Quál es el tipo de la siguiente función?

int una_función(char parámetro);

La solución es

int (*puntero_a_una_función)(char)

Para extraer el tipo de la declaración de función hemos hecho lo siguiente:

  1. Hemos eliminado los nombres de los parámetros.
  2. Hemos puesto un asterisco delante el nombre del tipo y hemos puesto todo esto entre paréntesis.
  3. Hemos reemplazado el nombre de la función por el nombre puntero_a_una_función.

Echemos un vistazo más detallado.

Los nombres de los parámetros ya no aparecen. El tipo de una función es el mismo, si el valor de retorno y si los parámetros tienen el mismo tipo. Los nombres que tienen no influyen. El nombre de la función desaparece también. El nombre de la función juega un papel similar como un valor constante para una variable. El tipo de la variable no depende del nombre de una constante.

El asterisco indica que el tipo es un puntero. Lo ponemos entre paréntesis por la preferencia de operadores del lenguaje. Sin ellas habría un error de sintáxis, porque la llamada de función (paréntesis tras un identificador) tiene mayor prioridad que el operador asterisco.

Finalmente hemos cambiado el nombre. Decir más correctamente, hemos introducido un nuevo nombre. Este nuevo nombre puntero_a_una_función representa un tipo que puede guardar la dirección de una función con el tipo especificado.

Como el uso de tipos de funciones es poco legible, se suele casi siempre definir un tipo de función mediante un typedef. Esto lo hacemos aquí también.

int una_función(char parámetro);

// Definición de una variable dirección_de_una_función
int (*dirección_de_una_función)(char) = NULL;

// Definición de un tipo con typedef.
typedef int (*TIPO_DE_UNA_FUNCIÓN)(char);

// Definición de una variable con el tipo de typedef
// Se inicializa la variable con la dirección de una_función.
TIPO_DE_UNA_FUNCIÓN otra_dirección = una_función;

Por cierto, algo así no se puede hacer:

// Error
int (*dirección_de_una_función)(char) =
    int una_función(char parámetro);

Esta línea haría algo como delcarar una_función y asignar su dirección al mismo instante. Esto no está permitido.

Usar un puntero de función

Tras saber expresar el tipo de una función nos interesa usarlo. Lo podemos usar directamente para definir una variable.

// Una función que ya conocemos.
int una_función(char parámetro);

// Definimos nuestro tipo de función y una variable.
typedef int (*TIPO_DE_UNA_FUNCIÓN)(char);
TIPO_DE_UNA_FUNCIÓN puntero_a_una_función = una_función;

// El valor de una variable se puede asignar a otra.
TIPO_DE_UNA_FUNCIÓN otro_puntero = puntero_a_una_función;

// Se pueden comparar punteros de funciones,
// pero no se pueden aplicar las operaciones aritméticas 
// de los punteros a constantes y variables.
const bool igual = 
    puntero_a_una_función == otro_puntero; // Bien
puntero_a_una_función++; // Error de compilación

// Y, desde luego, podemos llamar a una función
// Se puede usar dos sintaxis diferentes
const char parámetro = 'a';
const int resultado = puntero_a_una_función(parámetro);
const int resultado2 = (*puntero_a_una_función)(parámetro);

Que se puede llamar a una función con dos sintáxis diferentes es un tanto peculiar, ya que suele haber una diferencia importante entre en un puntero y un puntero dereferenciado. Pero compilan los dos.

La pregunta es: ?cuál de las dos formas es la preferible? Pues, es cuestión de gusto. Yo prefiero la forma (*puntero_a_una_función)() ya que deja más claro que punter_a_una_función el el nombre de una variable y no de una función.

Como C y C++ permiten conversiones entre todo tipo de punteros, se pueden convertir también los punteros de funciones a punteros de variables.

// Obtenemos un puntero de variable a mi función.
int una_función(char parámetro);
char* puntero_a_datos = (char*)una_función;

// Con este puntero_a_datos no sólo puedes leer el código
// ejecutable de la función. También puedes modificarlo.
puntero_a_datos[5] = 0xFF;

Es obvio, que estas conversiones son ideales para hackers, pero será poco probable que se necesitan durante un uso normal del lenguaje. Por eso puedes esperar que el compilador te avisará con insistencia. Además, el código ejecutable de una función es de sólo lectura. O debería. Conseguir asignar algo a la memoria del código ejecutable es una manera excelente para introducir un error que no se encontrará hasta que alguien intente llamar a esta función.

Ejemplos con más clase

Punteros, referencias y constantes aparecen tal cual en un tipo de función.

// Declaración de una función con const y referencia.
MiClase *const devuelve_mi_instancia(const std::string& nombre,
                                     int* puntero);
                                     
// Definición de una variable puntero_a_devuelve_mi_instancia
MiClase *const (*puntero_a_función)(const std::string&, int*);

// Mejor definir un tipo con typedef
typedef MiClase *const (*TIPO_FUNC)(const std::string&, int*);
TIPO_FUNC otro_puntero_a_función;

// Los punteros a funciones también pueden ser constantes
MiClase *const (*const ptr_const)(const std::string&, int*) =
    puntero_a_función;

// Entonces ya no se puede asignar otra función.
ptr_const = NULL;  // Error

También se pueden llamar a los métodos estáticos de una clase. Tienen el mismo tipo que funciones globales.

class MiClase
{
    // Si el método no es público, no lo podemos acceder desde
    // fuera.
public:
    // Declaración de un método estático sin parámetro
    static int método_estático(void);
};

// El tipo del primer método. Este método no tiene parámetros.
typedef int (*TIPO_MÉTODO)();

// Al contrario de una función global
// la dirección de un método de clase se extrae con ampersand
TIPO_MÉTODO puntero_a_método = &MiClase::método_estático;

// Se puede llamar como una función global
const int resultado = (*puntero_a_método)();

Conceptualmente, las funciones miembros no estáticos no tienen dirección sin una instancia de la clase. Por eso se deben referenciar con un objeto. Físicamente el mismo método se encuentra a la misma dirección para todos los objetos. No obstante, el valor del puntero this es distinto.

class MásClase
{
public:
    // Declaración de un método normal
    float método_normal(int);
    
    // Declaración de un método constante
    float método_const(int) const;
};

// El tipo para métodos no estáticos
typedef float (MásClase::*TIPO_VAR)(int);
typedef float (MásClase::*TIPO_CONST)(int) const;
TIPO_VAR puntero_a_método = &MásClase::método_normal;
TIPO_CONST puntero_a_método_const = &MásClase::método_const;

// Llamar a este método requiere un objeto, ya que
// el método no es estático.
MásClase objeto;
const int parámetro = 5;
float resultado = (objeto.*puntero_a_método)(parámetro);
resultado = (objeto.*puntero_a_método_const)(parámetro);

El TIPO_VAR permite también la asignación de un TIPO_CONST, igual como una variable permite la asignación de una constante.

Como ejemplo de ¡basta ya! terminamos con un puntero a un patrón de función miembro.

// Un patrón de clase
template 
class UnaClase
{
public:
    // Declaración de un patrón de método
    template 
    void método_template(ParamMétodo& salida);
};

// Definir una variable con el tipo del template
// El tipo del patrón del método no aparece salvo en los
// parámetros.
typedef void (UnaClase::*TIPO_TEMPLATE)(float&);
TIPO_TEMPLATE puntero_a_método_template =
    &UnaClase::método_template;

// Llamar a este método requiere un objeto, ya que
// el método no es estático.
UnaClase objeto;
float parámetro;
(objeto.*puntero_a_método_template)(parámetro);

Conclusión

En este artículo hemos tratado como se declaran y se usan punteros a funciones. No hemos hablado para qué se usan, ya que es otro tema relativamente largo.

En general es preferible no usar punteros a funciones cuando es posible porque complican el análisis del código: ya no se puede saber fácilmente el nombre de la función que se llama. En el depurado saldría más bien la dirección de memoria a que el puntero de función apunta.

Por eso preferible pensar en alternativas. Cuando se trata eligir entre pocas funciones conocidas a tiempo de compilación, se podría pensar en un switch que llama a estas funciones según una variable de estado. Quizá es menos elegante pero herramientas de análisis de código no se pierden tan fácilmente. En C++, además, se ofrecen «functores» – es decir clases que sobrecargan el operador (). Físicamente functores son objetos que se pueden asignar como variables. Por eso tienen un tipo bastante más legible que punteros a funciones.

Referencias

Lectura adicional

La directiva de preprocesador #include se usa en los lenguajes C y C++ para «incluir» las declaraciones de otro fichero en la compilación. Esta directiva no tiene más misterio para proyectos pequeños. En cambio, puede ayudar aprovechar bien esta directiva en proyectos con un gran número de subdirectorios.

El efecto de #include

Cuando el preprocesador encuentra una línea #include "fichero", entonces reemplaza esta línea por el fichero incluido. Así procede con todas las directivas de inclusión – también en aquellas anidadas en los fichero ya a su vez incluidos. Es decir, existe un sólo fichero grande tras la precompilación.

No obstante, esta unión de varios ficheros no tiene lugar físicamente. Lo que sucede es que se interrumpe la compilación del fichero actual, se compila el fichero incluido y, tras compilarlo, se continúa con el primero. Por eso, el compilador puede decirnos, en qué fichero tuvo lugar un error de compilación.

En cambio, conviene tener esta idea del fichero único en mente, porque a veces ayuda a encontrar errores. Uno muy típico es olvidarse el punto y coma tras la declaración de una clase.

En este caso hay una declaración de clase en el fichero incluido:

class MiClase {}

En el segundo puede haber algo así:

#include "mi_clase.h"
MiClase miInstancia;

En este ejemplo, el compilador se quejará de que aquí no se puede definir un nuevo tipo en la línea de MiClase miInstancia aunque esta línea es correcta. El error verdadero es la falta del ; en el fichero incluido. Lo que el compilador realmente ve es

class MiClase {} MiClase miInstancia;

No obstante, el programador no lo ve, porque el código está distribuido sobre dos fichero y el error se produce en el correcto.

La precompilación sólo modifica el código a nivel textual. No entiende del sintaxis del lenguaje. Por eso es posible distribuir el código de forma arbitraria. Por ejemplo, el siguiente ejemplo compilaría.

Fichero incluido

{
	int

Fichero principal

void main(void)
#include "fichero_incluido"
	a;
}

Esto es así porque el compilador ve el conunto

void main(void)
{
	int
	a;
}

La posición del #include

Lo habitual es posicionar las inclusiones al inicio de cada fichero. Esto tiene sentido, porque se suele requerir declaraciones básices antes de declarar clases más complejas. Mi lugar preferido en los ficheros de cabecera es tras el guardián de inclusión múltiple.

// Guardian de inclusión múltiple
#ifndef FICHERO_YA_INCLUIDO
#define FICHERO_YA_INCLUIDO

#include "declaraciones_basicas.h"

#endif

Así se evita que un compilador poco sofisticado abre otra vez el mismo conjunto de ficheros cuando se inluye un fichero de cabecera dos o más veces.

En los ficheros de definición (los .c o .cpp), los guardianes de inclusión múltiple no hacen falta. No obstante, puede haber una restricción importante cuando se usan cabeceras precompiladas. En este caso, todos los ficheros fuente deben incluir primero el mismo fichero de cabecera – que es aquello que define la cabecera precompilada. El compilador de C++ de Borland permite varios ficheros de cabecera para la definición de una cabecera precompilada. Estas inclusiones deben ser lor primeros ficheros incluidos y se deben incluir en el mismo orden.

Puede darse el caso de no poner las inclusiones en el inicio de un fichero. Esto es frecuente en los fichero que se podrían denominar «programados en directivas de precompilación». Normalmente se trata de ficheros de cabeceras con definiciones muy básicas como ajustes a la plataforma empleada. Por ejemplo, en medio de un fichero «definiciones_basicas.h» puede haber unas líneas

#if PLATAFORMA_ES_LINUX
#include "funcionalidad_gratis.h"
#elif PLATAFORMA_ES_MICROSOFT_WINDOWS
#include "funcionalidad_cara.h"
#elif PLATAFORMA_ES_APPLE
#include "funcionalidad_muy_cara.h"
#else
#error Esta plataforma no está soportada
#endif

La diferencia entre «» y <>

La directiva #include existe en dos versiones. En una se pone el nombre de fichero entre comillas, en la otra entre paréntesis angulares (el signo menor y mayor como «comillas»).

#include "fichero_con_comillas.h"
#include <fichero_entre_menor_y_mayor.h>

La versión con los paréntesis angulares busca los ficheros en todos los directorios que se han especificado en la llamada al compilador – normalmente con la opción «-I». Estos directorios se suelen rastrear por el fichero incluido en el orden en que aparecen en la línea de comando.

Cuando se incluye un fichero entre comillas, entonces el compilador busca este fichero primero en el mismo directorio que el fichero actualemente compilado y después en los demás directorios. Es decir, la versión con comillas se diferencia de la versión con paréntesis angulares únicamente por buscar primero en el directorio del fichero compilado. Tras no encontrarlo ahí actúa igual.

Esto muchas veces no es ninguna diferencia, ya que se suelen especificar todos los directorios en la línea de comando del compilador. Así no se suele dar el caso que se puede incluir un fichero con comillas pero no con paréntesis angulares.

Más significativo es el comportamiento ante ficheros con el mismo nombre en distintos directorios. En este caso la versión con comillas da preferencia sobre el fichero en el mismo directorio y esto suele ser el mejor acertado. Aunque sea preferible nombrar ficheros de forma única en un proyecto, es posible que no se pueda evitar tener dos ficheros con el mismo nombre cuando se incluyen varias bibliotecas de terceros.

De ahí se puede deducir que es imperativo incluir cabeceras de la misma biblioteca con comillas. De esta forma se puede asegurar que las cabeceras de una biblioteca se incluyan entre si aunque haya otros con el mismo nombre en uno de los directorios especificados en la línea de comandos.

Además, incluir con comillas puede dar al programador un significado adicional: que este fichero está bajo la custodia de mi equipo de desarrollo. Las cabeceras incluidas con paréntesis angulares son de bibliotecas de terceros. Los primeros ficheros puedo modificar si hace falta, los segundos no.

El orden de las inclusiones

El orden de las directivas #include no importa cuando todos los identificadores del programa son únicos. No obstante, a veces no lo son y conviene generar el error «este identificador ya existe» en nuestro código y no en el código de una biblioteca estándar.

Esto se consigue incluyendo primero las caberas de terceros. Si aparece un error de identificador doble, entonces aparece en la segunda definición – que es la nuestra – y ahí podemos cambiar el nombre del objeto sin problema.

En proyectos realmente grandes puede haber varias bibliotecas de distintas dependencias. Por la misma razón de generar los errores de identificadores dobles en el código más fácilmente modificable, conviene incluir las bibliotecas más básicas primero. Dentro del mismo nivel podemos ordenar los ficheros incluidos de forma alfabética. Esto ayuda a encontrar inclusiones que faltan o sobran.

El siguiente código muestra una secuencia de inclusiones para un fichero «definicion_de_mi_clase.cpp».

// Primero se debe incluir la cabecera de precompilación
#include "cabecera_de_precompilacion.h"

// Segundo, incluir la cabecera correspondiente
// a este fichero de implementación.
// Esto deja más claro, que es la
// clase que se implementa aquí.
#include "definicion_de_mi_clase.h"

// A continuación inclusiones de la biblioteca estándar.
// Se usan paréntesis angulares.
#include <vector>

// Inclusiones de otras bibliotecas de terceros
#include <wx.h>
#include <gl.h>

// Inclusiones de subbibliotecas básicas de
// mi proyecto con comillas
#include "mis_definiciones_basicas.h"

// Luego las demás inclusiones de mi proyecto
#include "clases_auxiliares.h"
#include "más_definiciones_para_mi_clase.h"

Usar rutas relativas

Una forma de evitar nombres de fichero dobles es incluir ficheros con rutas relativas.

#include "definiciones/tipos_básicos.h"
#include "funcionalidad/tipos_básicos.h"

La desventaja de esta forma es, que uno debe saber, en qué directorio se encuentra cada cabecera. No obstante, esto suele ser un problema menor. Sin ruta relativa, uno debería poner un prefijo a cada nombre de fichero para evitar nombres dobles. Estos prefijos son típicamente los nombres de los directorios. Es decir, todos los ficheros en el directorio «definiciones» tienen se llaman «definiciones_xxx». Al final debo saber el nombre de directorio de todas formas.

Los programadores de C++ han copiado de Java la idea de estructurar los directorios como los espacios de nombre. Así, el uso de la clase NombreDeEspacio::Subespacio::MiClase requiere la inclusión del fichero «nombre_de_espacio/subespacio/mi_clase.h».

El uso de rutas relativas en las inclusiones puede mejorar bastante el orden y reducir la configuración del compilador. Así basta incluir un solo directorio para obtener acceso a todos los componentes de la biblioteca boost, por ejemplo. En cambio, rutas relativas hacen más complicado recolocar ficheros a otros directorios. Y esto puede pasar a menudo en fases tempranos de un proyecto.

Optimizar la velocidad de compilación

La inclusión de un sólo fichero normalmente no afecta mucho al tiempo, que el compilador requiere para la compilación de un fichero. Pero esto se cambia si el fichero incluido incluye más ficheros que a su vez incluyen aún más ficheros. Gracias a tantas inclusiones anidadas, una sola inclusión, sí, puede cambiar el tiempo de compilación drásticamente.

Una forma de mejorar la velocidad es utilizar cabeceras precompiladas. Estas cabeceras deben estar incluidas por todos los ficheros de definición y, por eso, debe ser una decisión temprana en un proyecto de utilizarlas o no – al menos si uno quiere evitar añadir esta inclusión en muchos ficheros.

Otra forma es evitar la inclusión de ficheros no necesarios. Normalmente una inclusión adicional no perjudica al resultado de la compilación. Simplemente añade declaraciones que finalmente no se usan. Pero la compilación de estas declaraciones cuesta un tiempo que nos queremos ahorrar.

Para proyectos grandes puede convenir no incluir definiciones de tipos que sólo aparecen como punteros o referencias en los ficheros de cabecera. Esto tipos se pueden introducir con una declaración forward. Sólo en el fichero de definición se incluye entonces la cabecera que define este tipo.

Como ejemplo damos una clase, que guarda un puntero a otra clase. En el fichero de cabecera tenemos

// Declaración forward
class OtraClase;

// Declaración de la clase principal
class UnaClase
{
	OtraClase* dame_un_puntero_a_otra_clase();
};

En el fichero de definición tenemos

#include "una_clase.h"
#include "otra_clase.h"

OtraClase* UnaClase::dame_un_puntero_a_otra_clase()
{
	static OtraClase otraInstancia;
	return &otraInstancia;
}

Cabe señalar que una declaración forward debe tener lugar dentro del nombre de espacio correspondiente.

// Esto no es correcto
class NombreDeEspacio::Subespacio::Clase;

// Se debe hacer la declaración forward así
namespace NombreDeEspacio
{
	namespace Subespacio
	{
		class Clase;
	}
}

Conclusión

Como hemos comprobado, hay bastante que se puede tener en cuenta sobre la directiva #include, cuando se trabaja en proyectos grandes.

  • La inclusión genera un sólo fichero grande. Tener esto en mente cuando se produce un error de compilación en la primera línea aparamente correcta tras un #include.
  • Conviene incluir ficheros del propio proyecto con comillas y las cabeceras de terceros con paréntesis angulares para marcar la diferencia entre «propio» y «estándar».
  • Ordenando las propias inclusiones al final ayuda a corregir errores de doble definición de identificadores.
  • Rutas relativas son una buena opción para proyectos con una clara estructura de directorios.
  • Usar cabeceras precompiladas y evitar incluir ficheros innecesarios – también aprovechando declaraciones forward – puede acelerar el proceso de compilación.

Lectura adicional

Asignar null o cero a referencias o manejadores, que no apuntan a ningún objeto válido, es una forma eficaz de evitar errores difíciles de trazear. En este artículo presento por qué es una buena idea «anular» referencias no utilizadas y como anulamos referencias para que a primera vista no parece existir un valor nulo.

Qué son referencias

En este artículo usamos el término «referencia» para varios tipos datos. Pueden ser punteros a memoria dinámica o manejadores (handles en inglés) de recursos. También pueden ser variables de objetos como en Java. Lo que todas estas entidades tienen en común es que no son un recurso sino lo representan.

Cuando se copia una referencia, hay un apuntador más al mismo recurso, pero no se copia el recurso en si. Cuando se libera un recurso, entonces todas las referencias a él se vuelven inválidos. Los problemas que pueden surgir cuando usamos referencias inválidas discutimos a continuación.

Punteros

Antes de hablar de referencias en general, quiero explicar unos conceptos básicos con el caso estrella de liarse: los punteros de C y C++. Punteros son variables que guardan una dirección en la memoria. Cuando se reserva memoria, entonces se guarda la dirección de este memoria en un puntero.

Memoria dinámica en C++

Un escenario típico para la creación de memoria dinámica es el siguiente.

// Reserva memoria - en C se haría con malloc
MiObjeto* puntero_a_objeto = new MiObjeto();

// Haz algo con los datos del puntero

// Libera la memoria
delete puntero_a_objeto;

¿Por qué el código arriba está mal? Pues, realmente no es. La memoria se reserva con new (o malloc en C) y se libera correctamente con delete (con free en C). No obstante, no se asigna NULL al puntero_a_objeto. Y esto puede causar problemas.

Por ser completos, también quiero decir, cuando no da problemas: cuando se pierde el acceso a la variable puntero_a_objeto porque se sale del bloque donde se definió. Este caso se da a menudo en destructores, donde no anular un puntero no suele causar problemas.

Apuntar al vacío

En muchas ocasiones se usa la comparación puntero == NULL para comprobar, si un puntero apunta a nada. Si no es NULL se lee y escribe en la memoria a que apunta. A veces un programa espera hasta que se haya liberado una memoria. Un puntero con el valor NULL representa esta liberación. Por lo tanto, olvidarse a anular un puntero inválido puede causar un bucle infinito. El programa espera a la liberación de la memoria, pero no se entera porque no se anuló el puntero correspondiente.

Aunque sea liberada, la memoria no desaparece. (Esto sólo pasa si sacas la memoria RAM de tu ordenador.) Como todavía existe, se puede leer de ella. Por eso, un objeto liberado puede parecer vivo y se puede emplear al menos hasta que se reserve la memoria de nuevo.

Apuntar mal

Cuando se reserva la memoria liberada de nuevo, se suele sobrescribirla con otros valores en general. Entonces pueden producirse dos casos:

Los nuevos valores no son útiles para el objeto anteriormente liberado. Por eso se lee valores malos con el puntero «olvidado» (olvidado a asignarle NULL). Un valor inútil puede ser, por ejemplo, índices de array más allá del tamaño del array. En este caso suele producirse un error en el programa en tiempo de ejecución.

El problema es encontrar este error: en el objeto anteriormente liberado parece cambiarse un índice de array así de repente y sin que exista algún instrucción correspondiente. Y ahora a adivinar por qué. El nuevo objeto que se guardó en la memoria liberada puede estar definido en una biblioteca de que ni siquiera tienes el código. Y la función en que deberías haber anulado el puntero puede estar en otro fichero.

El segundo caso es aún peor: el nuevo objeto cambia el contenido de la memoria pero los valores todavía resultan «correctos». Por ejemplo, un índice de array vale uno en lugar de cinco. Con esto el programa puede seguir funcionando aunque más tarde o temprano algo parece estar mal.

Más tarde puede ser días o meses. ¡Imagínate que el índice apunta a un array de datos personales y de repente lees los datos de otra persona! Se envía una multa de tráfico a alguien, aunque el policía nunca lo editó así. Y todo esto por usar un puntero inválido por haberse olvidado de asignarle NULL cuando se liberó.

La regla de oro de punteros liberados

En conclusión: Si liberas memoria, asigna NULL al puntero. Sin pensar. Y aunque sea la última instrucción en un destructor. Por 99 veces que lo haces en vano, esta una que te salva de un error complicado lo vale.

Diferencias entre compilación debug y release

Para evitar desastres mayores con la memoria liberada, algunos compiladores incluyen código en modo debug, que sobrescribe la memoria liberada con algún valor tonto como 0xCCCCCCCC. Con suerte es un valor lo suficientemente inútil, para que se genere un error al usarlo de forma inmediata.

No obstante, sobrescribir cuesta tiempo y esto se quiere ahorrar. Por eso, en un programa compilado en modo release, la memoria se queda inalterada tras liberarla. Y por lo tanto se puede seguir leyendo de ella como si fuera válida. Es una de las razones porque un programa puede comportarse de forma diferente en modo debug y en modo release.

Referencias en general

La física de una referencia

Los punteros son un tipo de referencias, pero todavía representan algo físico: una dirección de memoria. Una referencia en general no está atada a una posición específica en la memoria. Sólo representa «algo» que suele estar encapsulado en una biblioteca.

Por ejemplo, cuando se crea una ventana con la biblioteca X, la función de creación devuelve un identificador que representa esta ventana. Hay que pasar este identificador como parámetro cuando se llama a una función de la biblioteca X. Dentro de la biblioteca se usa el identificador para buscar, en qué dirección de memoria se guarda la ventana y se modifica según la función llamada.

La imensa mayoría de referencias son números enteros aunque no aparecen como tales. Una variable objeto en Java o C# se comporta como un objeto de este tipo, pero internamente es un número entero que se el intérprete usa como índice en una lista de objetos.

De hecho, instancias de objetos y referencias tienen características en común. Una referencia o manejador representa una instancia de algo igual como una variable de objeto. Por ejemplo, cuando se usan sockets en en lenguaje C, se representa un socket específico por un socket id – que es un número entero. En un lenguaje orientado a objetos como Java existe una clase Socket. Se crea una conexión de socket al crear una instancia de esta clase.

Analogía entre referencias y punteros

Lo que equivale a «reservar memoria» para un puntero, es la creación de un objeto y el acceso a un recurso para una referencia o un manejador. Se llama a una función Create, que devuelve un identificador para el nuevo recurso creado, y al final se llama a una función Free para liberar este recurso.

Normalmente existe un valor que indica que una referencia o un manejador no referencia nada. En una referencia numérica suele ser cero, una variable objeto puede contener el valor null o nullptr. Tras liberar un objeto, se debería asignar este valor nulo a la referencia, ya que ahora ya no apunta a nada. Si no, las consecuencias puede ser parecidas a las de los punteros «olvidados» mencionados arriba.

Un caso real

Cuento un caso real. Con la biblioteca X Toolkit se pueden crear temporizadores. Cuando vence el tiempo establecido, la biblioteca llama a una función callback y tras esto, el temporizador ya no existe.

Una clase que usa uno de estos temporizadores guarda la referencia a ella en una variable miembro. Puede ofrecer el servicio de cancelar un temporizador. Si el valor de la variable no es cero, entonces se cancela.

Ahora nos imaginamos que el temporizador ya ha vencido y, en consecuencia ya no existe, pero no hemos anulado la referencia en la función callback. Más tarde cancelamos el temporizador. Como la variable miembro no vale cero, se cancela.

En un caso favorable, la biblioteca X Toolkit devuelve un error que este identificador ya no existe. En un caso menos favorable, el identificador, sí, existe, porque se ha vuelto a asignar para otro temporizador. Como existe, la cancelación elimina un temporizador totalmente distinto.

Como el temporizador ya no existe, ya no vencerá, y se asignará al programador correspondiente la tarea de corregir su error, cuando él no ha hecho nada mal. Y ahora imagínate que este programador eres tú y te toca tener la idea que este error puede ser el problema y, a continuación, rastrear mil ficheros de código para ver, en qué callback se olvidó anular la referencia al temporizador.

El valor NULL en cada caso

En fin, no pienses más. Si liberas un recursos, anula las referencias correspondientes. Siempre y sin pensar.

A veces no es muy obvio que es el valor nulo. Por ejemplo, ¿qué es el valor nulo para un iterador de la biblioteca estándar de C++? En este caso se le podría asignar el valor que devuelve el método end() del contenedor correspondiente.

El problema de adivinar cuál es el valor nulo suele aparecer cuando la referencia es un tipo opaco: algún tipo que devuelve una función de creación, pero que no permite asignarle el número entero cero.

Si una biblioteca no aparenta tener un valor nulo para estas referencias, entonces se puede hacer un truco. Se encapsula el identificador en una clase. Sólo es posible crear una instancia de esta clase creando una instancia del objeto que se quiere referenciar. El destructor de la clase libera el recurso. Es decir, la referencia no se anula sino desaparece con la clase que la encapsula.

Si el lenguaje no permite programación orientado a objetos, entonces se puede utilizar una variable booleana adicional, que indica si la referencia es válida o no.

Conclusión

Hemos visto, qué líos pueden surgir por referencias y punteros no anulados tras liberar el recurso a que apuntaban. Suelen causar errores que requieren mucha experiencia en fijarlos. Por eso hemos llegado a la conclusión, que siempre hay que evitar referencias inválidas asignándoles algún valor nulo. En pocos casos el valor nulo puede ser no obvio. Entonces se puede encapsular la referencia en una clase o se usa una variable booleana que indica la validez de la referencia.

Lectura adicional

Menos errores
Más estilo
Más conceptos

Introducción

Con funcioncitas me refiero a funciones de una o dos líneas, muchas veces con un cálculo muy simple, pero con un valor semántico muy potente.

Escribir una función adicional fracasa a menudo en una cierta pereza, porque hay que buscar un hueco en el fichero fuente, escribirla, en C y C++ hay que añadirlo en la cabecera, eventualmente serás capaz de escribir algún comentario de qué esta función debe hacer y todo esto para una línea de código bastante trivial. Pero esto se puede ver también de otra forma: escribes el código una vez, lo modificas diez veces y lo lees cien veces. Si tardas 99 segundos (1 minuto y 39 segundos) para escribir una función que te ahorra un segundo en la lectura, todavía ahorras tiempo.

El valor de las funciones pequeñas está en su claridad semántica, que mejora mucho la legibilidad del código – sobre todo cuando se trata de una combinación de varias funciones pequeñas. Para ilustrar esto, presento algunos ejemplos.

Copiar con punteros

Si p y q son dos punteros, entonces ¿qué hace el código de C siguiente?

while (*p++ = *q++) {}

Lo que hace es copiar el valor a que q apunta a la memoria referenciada por p. A continuación pasa ambos punteros al próximo y elemento y sigue copiando hasta que haya copiado un cero. (El valor de una asignación es el valor asignado, el cero se interpreta como un false y el bucle while termina.)

Tenemos una expresión booleana basada en cualquier tipo arbitrario que el compilador sepa amablemente interpretar como false o true y que, además, modifica las variables dentro de la expresión. A parte de tener un buen ejemplo de un uso sofisticado pero poco intuitivo del lenguaje de programación C, ¿para qué se podría utilizar un bucle así?

Pues, para copiar un string. En la biblioteca estándar de C hay una definición de strcpy similar a esta (la versión estándar devuelve un char*):

void strcpy(char *p, const char *q)
{
    while (*p++ = *q++) {}
}

Ahora la pregunta: ¿qué te parece más comprensible? ¿Una línea strcpy(p, q) o un bucle while (*p++ = *q++) {}? Técnicamente no trae ninguna ventaja llamar a la función strcpy, pero semánticamente, es decir, el significado del código es mucho más claro para el programador.

Funciones pequeñas son a menudo parte del estándar. La clase String en C# tiene un método IsNullOrEmpty. La expresión

if (!String.IsNullOrEmpty(miString))

reemplaza a

if (miString != null && miString.Length > 0)

Es lo mismo, pero el método IsNullOrEmpty deja la intención más obvia.

Acotar rangos

Un ejemplo simple. Mira el reloj cuánto tardas en contestar a la pregunta. ¿Qué representa c?

c = a < b ? a : b;

Y ahora los mismo otra vez. ¿Qué representa c?

c = min(a, b);

Esto era fácil, pero ¿qué tal con algunos niveles de anidamiento?

c = c < a ? a : (c > b ? b : c);

Esto limita c al rango [a, b]. Si no estás tan firme en el uso del operador ? : podrías escribir la línea anterior con if.

if (c < a)
{
    c = a;
}
else
{
    if (c > b)
    {
	    c = b;
	}
}

Un poco más amigable sería usar las funciones pequeñas min y max:

c = max(a, min(b, c));

Mi opción preferida sería una función

c = limitToRange(a, c, b);

donde el parámetro en medio será acotado al intervalo [a, b].

Puedes optar por hacer una pequeña función que ocupa una línea o la construción con el if anidado arriba que ocupa once líneas por cada variable que quieres acotar. Y puede haber muchas: si c es un índice de un array, entonces podrías tener la necesidad de acotarlo cada vez que accedes al array.

Algo parecido vale si quieres generar un error al recibir un índice fuera de rango. La manera directa sería escribir algo así

if (index < start || index >= end)
{
    std:ostringstream mensaje;
	mensaje << "Error de índice index = "
	        << index
			<< ". Fuera del rango ("
			<< start
			<< ", "
			<< end
			<< ").";
    throw std::exception(mensaje.str());
}

Pero igual vale la pena hasta escribir una clase de excepción propia.

if (indexOutOfRange(start, index, end))
{
    throw IndexException(start, index, end);
}

Conclusión

Funciones pequeñas aumentan considerablemente la legibilidad del código. Ayudan a reducir el número de líneas dedicadas a comprobar la calidad de los datos y dejan a consecuencia más espacio a la parte principal de una función. Para no perder velocidad en la ejecución, C++ ofrece definir estas pequeñas funciones inline.

Muchas veces las funciones pequeñas son tan generales que se pueden usar en todos los proyectos de programación. Una buena gestión debería procurar que se mantiene una biblioteca que todo el mundo en la empresa pueda usar. Como son funciones muy básicas, conviene también pensarse bien sus nombres y el espacio de nombres en que se encuentran.

Si no eres gerente sino un simple programador, entonces te recomiendo que guardes estas funciones en un fichero personal. Así puedes copiar y pegarlas en cualquier proyecto en que participas. De hecho, mantener una biblioteca personal puede facilitar tu trabajo.

Referencias

Cuando compilamos patrones de clases (templates) en C++, como los contenedores de la STL, entonces tenemos a veces errores de compilación con mensajes exageradamente largos, que esconden más que aclaran. En este artículo quiero enseñar de como «leer» estos mensajes para fácilmente encontrar el error causante.

Primero debemos entender que el mensaje del compilador intenta mostrar toda la información posible. Según el tipo del problema, diferentes partes del mensaje pueden ser significantes. Por ejemplo, si cambio el allocator estándar de un contenedor por uno diferente, entonces me importará qué dice el mensaje de error sobre este componente. No obstante, en este artículo asumimos un uso básico de contenedores de la STL.

Más concretamente compilamos este trozo de código.

    // Una instancia de un mapa
	std::map mi_mapa;

	// Un iterator a una posición del mapa
	std::map::const_iterator const_itr = mi_mapa.begin();

	// Insertar un nuevo elemento en la posición indicada por el iterador
	mi_mapa.insert(const_itr, std::pair("atributo", "valor"));

Este código falla en la línea mi_mapa.insert.

Descifrando un mensaje de MinGW

Compilando con un compilador MinGW obtenemos el siguiente error para esta línea.

error: no matching function for call to `std::map, std::allocator > >::insert(std::_Rb_tree_const_iterator >&, std::pair)'
C:/opt/Dev-Cpp/include/c++/3.4.2/bits/stl_map.h:360: note: candidates are: std::pair<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::insert(const std::pair&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]
C:/opt/Dev-Cpp/include/c++/3.4.2/bits/stl_map.h:384: note:                 typename std::_Rb_tree<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator std::map<_Key, _Tp, _Compare, _Alloc>::insert(typename std::_Rb_tree<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator, const std::pair&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]

El mensaje consiste de tres líneas: primero el error de que no hay una función que corresponde a la llamada y luego dos propuestas del compilador que funciones se podría haber intentado llamar. Como nosotros no hemos escrito la función a que llamamos, sabemos que nuestra llamada debe ser mala.

El truco para descifrar este mensaje largo consiste en eliminar sucesivamente los parámetros de los patrónes (template parameters). Nos fijamos en la primera línea lo que está entre las comas altas (‘): std::map, std::allocator > >::insert(std::_Rb_tree_const_iterator >&, std::pair).

Empezamos a eliminar desde dentro de las paréntesis angulares < > todo lo que nosotros no hemos especificado en ningún sitio. Sí, correcto, simplemente lo borramos. Son parámetros por defecto que no nos interesan. Hemos definido una instancia de std::map. En el mensaje de error, estos dos parámetros de tipo string aparecen, pero luego seguido por un std::less que no hemos puesto en ningún sitio. ¡Así fuera! Lo mismo pasa con el std::allocator. Lo borramos. Haciendo esto nuestro mensaje de error queda más corto: std::map::insert(std::_Rb_tree_const_iterator >&, std::pair).

El método insert tiene dos parámetros. Correcto. El último, es realmente un std::pair, pero el primero es un _Rb_tree_const_iterator de que no sabemos de donde procede. Nuestro primer parámetro en un const_iterator. Pues bien, _Rb_tree_const_iterator contiene la palabra const_iterator y será un tipo interno para representar un iterador a constante a un mapa. ¿Pero por qué tree? Aquí nos ayuda saber que los contenedores asociativos (std::set, std::map, std::multimap) están internamente organizado como un árbol – que es un tree en inglés. Si sustituimos la palabra tree por map, entonces el primer parámetro del método insert tendra el tipo std::_Rb_map_const_iterator >. Como nuestro tipo de iterador no tiene parámetros de patrón, simplemente lo borramos y nos queda nada más que std::_Rb_map_const_iterator.

La primera línea del mensaje de error queda entonces en

std::map::insert(std::_Rb_map_const_iterator&, std::pair)

Esto se parece a la línea donde se produjo el error:

mi_mapa.insert(const_itr, std::pair("atributo", "valor"));

Este parecido entre si no es de extrañar, ya que el mensaje de error hace referencia justamente a esta llamada.

 

Ahora nos fijamos en la segunda línea que contiene una propuesta de función y la acortamos de la misma manera.

C:/opt/Dev-Cpp/include/c++/3.4.2/bits/stl_map.h:360: note: candidates are: std::pair<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator, bool> std::map<_Key, _Tp, _Compare, _Alloc>::insert(const std::pair&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]

Primero eliminamos todo el texto de introducción hasta «candidates are:». Luego eliminamos los _Compare y _Alloc que nosostros no hemos pedido y nos quedamos con esto:

std::pair<_Key, std::pair, std::_Select1st >>::iterator, bool> std::map<_Key, _Tp>::insert(const std::pair&) [with _Key = std::string, _Tp = std::string]

Recordamos que podemos reemplazar tree por map y renombamos _Rb_tree a _Rb_map. Los parámetros de patrón contiene un conjunto de tipos que no nos interesa, así simplemente lo quitamos y nos queda

std::pair std::map<_Key, _Tp>::insert(const std::pair&) [with _Key = std::string, _Tp = std::string]

Si nos ayuda podemos reemplazar _Key y _Tp por std::string como indica la última parte del mensaje. El nombre de espacio std y la palabra clave typename no nos aportan tampoco información, así los eliminamos también.

pair<_Rb_map::iterator, bool> map::insert(const pair&)

Finalmente vemos claro que la función propuesta es un método insert que tomo un tipo pair como entrada.

 

Hacemos el procedimiento a la tercera línea del mensaje de error.

C:/opt/Dev-Cpp/include/c++/3.4.2/bits/stl_map.h:384: note:                 typename std::_Rb_tree<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator std::map<_Key, _Tp, _Compare, _Alloc>::insert(typename std::_Rb_tree<_Key, std::pair, std::_Select1st >, _Compare, _Alloc>::iterator, const std::pair&) [with _Key = std::string, _Tp = std::string, _Compare = std::less, _Alloc = std::allocator >]

Eliminamos los parámetros de patrones innecesarios, reemplazamos tree por map, sustituimos _Key y _Tp por std::string, y eliminamos std y typename. Y con esto llegamos a esta función propuesta:

_Rb_map::iterator std::map<string, string>::insert(_Rb_map::iterator, const pair&)

Esta propuesta ya se parece bastante a nuestra llamada

mi_mapa.insert(const_itr, std::pairstring, std::string>("atributo", "valor"));

Lo que es diferente es el primer parámetro: nosotros usamos un const_iterator mientras la función propuesta requiere un iterator sin const. Si cambiamos la línea

std::mapstring, std::string>::const_iterator const_itr = mi_mapa.begin();

por

std::mapstring, std::string>::iterator ya_no_const_itr = mi_mapa.begin();

entonces compila.

Entender a Microsoft Visual Studio

Como ejercicio puedes usar las mismas técnicas para entender el resultado de compilación del mismo código con Microsoft Visual Studio.

Error	1	error C2664: 'std::_Tree<_Traits>::iterator std::_Tree<_Traits>::insert(std::_Tree<_Traits>::iterator,const std::pair<_Ty1,_Ty2> &)' : no se puede convertir el parámetro 1 de 'std::_Tree<_Traits>::const_iterator' a 'std::_Tree<_Traits>::iterator'

error C2664: 'std::_Tree<_Traits>::iterator std::_Tree<_Traits>::insert(std::_Tree<_Traits>::iterator,const std::pair<_Ty1,_Ty2> &)' : no se puede convertir el parámetro 1 de 'std::_Tree<_Traits>::const_iterator' a 'std::_Tree<_Traits>::iterator'
1>        with
1>        [
1>            _Traits=std::_Tmap_traits,std::allocator>,false>,
1>            _Ty1=const std::string,
1>            _Ty2=std::string
1>        ]
1>        and
1>        [
1>            _Traits=std::_Tmap_traits,std::allocator>,false>
1>        ]
1>        and
1>        [
1>            _Traits=std::_Tmap_traits,std::allocator>,false>
1>        ]
1>        No hay disponible ningún operador de conversión definido por el usuario que pueda realizar esta conversión, o bien no se puede llamar al operador

Conclusión

Al inicio se pueden copiar los mensajes de error largos a un editor de texto y modificar ahí sucesivamente el mensaje hasta sólo queda la parte significativa. Con un poco de práctica, uno consigue hacer estas reducciones en la mente. La mayor parte del mensaje está «contaminado» por parámetros de patrones por defecto, que se necesitan por razones técnicas pero que no se usan como usuario de una clase.

La experiencia enseña también, que la mayoría de los errores son del mismo estilo: mezclar iterator y const_iterator o confundirse en los parámetros o valores de retorno de métodos sobrecargados.

Referencias

Introducción

Igual como la palabra clave const, el modificador static trae confusiones porque se puede utilizar en varios contextos en C y C++. Podemos declarar estáticas funciones y métodos. Variables pueden estar estáticas dentro de funciones, métodos, clases y a nivel global, es decir, por todas partes.

Variables estáticas locales

Lo más común es declarar una variable local estática:

void mi_funcion_o_metodo(void)
{
    static int var_estatica = 4;
}

El valor de la variable var_estatica no se pierde aunque ya no se encuentre dentro del alcance, es decir, cuando el programa haya salido de la función en que la variable fue definida. Esto diferencia una variable estática de una variable no estática, cuyo valor se pierde al final de la función.

Debido a esta diferencia, las variables estaticas no se guardan en el stack sino en una memoria especial para variables estáticas. Aunque no sean accesibles durante toda la ejecución del programa, las variables estáticas existen durante toda la ejecución del programa.

Igual como en el caso de las variables convencionales, se llama al constructor de las variables estáticas cuando se construyen. Esto succede una vez antes de llamar a la función main. Igualmente se llama una vez al destructor después de salir del programa. Recuerda que las variables estáticas existen durante toda la ejecución del programa. Por eso deben inicalizarse antes de entrar y después de salir de la función principal.

No hay diferencia entre variables estáticas en funciones y métodos de clases. De hecho, una variable estática se puede definir dentro de cualquier bloque delimitado por {}.

Miembros estáticos de una clase

Variables miembros de una clase también pueden ser estáticas. Una variable estática de una clase es un dato común a todas las instancias de esta clase y accesible en todos los métodos. Una variable miembro estática es incluso accesible sin instancia alguna.

En el código siguiente definimos una clase con una variable miembro estática:

class MiClase
{
public:
    static int mi_variable;
};

Esta variable se inicializa así:

int MiClase::mi_variable = 5;

Si la queremos usar sin instancia, entonces usamos el nombre cualificado:

void haz_algo(void)
{
    MiClase::miVariable = 3;
}

Lógicamente, la variable miembro debe ser pública para que la podamos usar fuera de los métodos de la clase. (Dentro de los métodos de la clase no hace falta poner el nombre de la clase delante.)

Las variables miembros estáticas existen también durante toda la ejecución del programa. Su diferencia con las variables estáticas definidas en una función o método está en su alcance: Las variables miembros están al alcance de todos los métodos de una clase y no sólo de uno.

Constantes estáticas

Variables estáticas se pueden declarar también const. Una constante estática es una constante universal del programa, pero de alcance limitado. Es una buena práctica utilizar constantes estáticas en lugar de macros #define ya que las constantes llevan un tipo: un 5 puede ser un int, un char o un float pero un dato

const static int var = 5;

siempre es un int.

Métodos estáticos

Métodos de clases también pueden ser estáticas. Declarar un método estático es una promesa que este método sólo utiliza miembros estáticos de la clase. Este tipo de método no conoce el puntero this, por lo cual no se puede hacer directamente referencia a métodos y variables no estátitocs de la clase.

class MiClase
{
public:
    // Una variable estática
    static int una_variable_estatica;

    // Una variable no estática
    int una_variable_no_estatica;

    // Un método estático
    static void haz_algo(void)
    {
        // Se pueden usar variables estáticas
        una_variable_estatica = 5;

        // Error: no se pueden usar variables que
        // requieren un puntero this
        una_variable_no_estatica = 5;  // Error de compilación
    }

    // Un método normal (no estático)
    void haz_otra_cosa(void)
    {
        // Se pueden usar variables y métodos estáticos
        // también en métodos no estáticos
        una_variable_estatica = 5;
        haz_algo();

        // Una variable de instancia en un método de instancia
        una_variable_no_estatica = 5;  // Correcto
    }
};

Es posible llamar a métodos estáticos de una clase sin tener una instancia de ella utilizando el nombre cualificado.

void una_funcion_cualquiera(void)
{
    // Correcto: Se puede llamar a un método no estático
    // si instancia
    MiClase::haz_algo();

    // Sin embargo, también se le puede llamar con instancia
    MiClase mi_instancia;
    mi_instancia.haz_algo();

    // No es posible llamar a un método convencional
    // sin instancia
    MiClase::haz_otra_cosa();  // Error de compilación
}

Métodos estáticos son similares a funciones globales. Sólo llevan el nombre de la clase como prefijo. (Un nombre de espacio no se usa de forma diferente que una clase con todos los miembros públicos y estáticos.)

Métodos estáticos pueden sobrecargar los nombres métodos no estático. De hecho es una práctica bastante común – sobre todo para métodos recursivos. Como ejemplo podemos considerar una clase String, en que un método convencional afecta a la instancia de una clase, mientras el método estático a un parámetro.

class String
{
public:

    // Crea una instancia con todas las letras del parámetro 
    // convertidas a minúsculas.
    static String a_minusculas(const String& instancia_de_string);

    // Convierte todas las letras de ESTA instancia a minúsculas
    void a_minusculas(void)
    {
        *this = a_minusculas(*this);
    }
};

Enlace interno

Finalmente es posible declarar estáticos funciones y variables globales, es decir, fuera de una función. Una variable a nivel global existe durante toda la ejecución del programa sea estática o no. Y cualquier función tiene acceso a una variable global. Por eso static tiene otro significado fuera de un bloque {}: especifica que el identificador tiene enlace interno.

Enlace interno quiere decir, que el linker no puede usar este nombre para enlazarlo con otro módulo. Es un nombre privado en el fichero de objeto (.o o .obj). El contrario es un enlace externo: El fichero objeto expone este nombre y su tipo al linker que puede usar esta información para enlazarlo con una referencia en otro módulo.

El modificador static en una función o variable suprime el enlace externo. Se puede forzar un enlace externo con el modificador extern. Por defecto, funciones tienen un enlace externo y variables interno. Por eso no es necesario declarar una función extern y una variable static, pero mejora la semántica del código.

Conclusión

Hemos visto que la palabra clave static se usa para declarar variables de un alcance limitado pero con una vida durante toda la ejecución del programa. Métodos estáticos se limitan a acceder a variables miembros estáticos de la clase. Finalmente se usa el modificador static para forzar un enlace interno.

Como última nota cabe mencionar, que el uso de variables estáticas puede complicar la creación de procesos multi-hilo, ya que estos datos son comunes a todos los hilos.

Referencias

Ya hace tiempo que el lenguaje de programación Java ha destronado a C++ como el lenguaje más popular. No extraña que recién titulados carecen de conocimientos profundos sobre C++ y están mal vistos por veteranos que carecen de conocimientos en Java.

Érase una vez que estos veteranos eran los recién titulados y se enfrentaron con problemas similares aunque con lenguajes de programación distintos. Antes del paradigma de orientado a objetos se disfrutó de un lenguaje bien estructurado – donde lo nuevo era el bien: El artículo Real Programmers Don’t Use Pascal fue escrito cuando Java se llamaba Pascal y C++ Fortran. No te dejes escapar el enlace a la historia de Mel dentro de este artículo para los tiempos pre-informáticos cuando el lenguaje Fortran era el Java y el C++ de entonces no tenía ni nombre.

Aunque C++ ya no es el lenguaje más popular, su ráiz, el lenguaje C, está todavía presente en el fondo de todas las plataformas. Un artículo clásico sobre como una broma se hizo seria es Creators Admit Unix, C Hoax.

Aunque a primera vista puede parecer algo para teorías académicas, se comprueba que los lenguajes esotéricos de programación son algo para gente creativa con el tiempo suficiente para escribir programas en Brainfuck, dibujarlas (!) en Piet o hacerlos comprensibles para orang utans en Ouk. También en la forma de desarrollar un tema clásico, los programadores no necesitan quedarse fuera en los premios Nobel de la literatura, aunque sea para decir algo tan simple como hola mundo.

Y finalmente una explicación, porque ya no podemos seleccionar la zona temporal en Microsoft Windows con un mapa del mundo. Pues, es posible que esto incluso ha evitado conflictos fronterizos armados.

Muchas veces necesitamos reservar memoria dinámica durante la ejecución de un programa. Sin embargo, esto es un proceso relativamente lento, porque el sistema operativo debe buscar un hueco suficientemente grande en el heap, donde se almacenan los objetos dinámicos. Esto se nota, por ejemplo, cuando uno quiere reservar muchos elementos de una lista. En este artículo quiero presentar algunas propuestas, como lenguajes compilados ofrecen alternativas más veloces. Los ejemplos de código serán en C++.

Contenedores pueden instanciar varios elementos de golpe

El tamaño no importa (mucho) a la hora de reservar memoria. Es el número de veces que afecta más. Una primera posibilidad que se ofrece, por lo tanto, es reservar memoria para varios elementos de golpe. Por supuesto, esto es menos optimal respecto al uso de la memoria, pero una técnica ampliamente usado por los contenedores de la biblioteca estándar de C++ (STL).

Un contenedor puede usar el operador new[] estándar para reservar un array de, digamos, 10 elementos y luego gestionar, cuales elementos ya están en uso y cuales no. Esto puede ser relativamente complicado. Sin embargo, se puede hacer a medida para una clase contenedor y también cuando le haga falta. Por lo tanto, es una buena opción.

Sobrecargar el operador new.

Otra opción para una clase, que está ideada para instanciarse a menudo de forma dinámica, es sobrescribir el operador new (y delete) de esta clase. La implementación de este operador sobrecargado suele tener dos elementos. Primero usa el operador new global para pedir memoria al sistema operativo. Segundo, las instancias reservadas se gestionarán por unas variables estáticas de la clase. De esta forma, el depósito de elementos reservados no depende de una instancia en concreto – que es justamente lo que queremos.

El operador new global debe ser precidido por el operador :: para distinguirle del operador sobrecargado. Es decir new MiClase() llamaría al operador sobrecargado, mientras ::new MiClase() al sistema operativo. Si el operador no está sobrecargado, ambas notaciones tienen el mismo efecto. Lo mismo vale para el operador delete.

Separar reserva de memoria y construcción de instancia

La desventaja en pedir muchos elementos de golpe es que se llama el constructor por defecto para todos estos elementos aunque no se usan a priori. Pero a esto hay remedio, porque en C++ se puede separar la reserva de la memoria dinámica de la construcción de una instancia. Si, en lugar del tipo de la clase, reservo un array de char, entonces la reserva no llama a ningún constructor. Cuando quiero instanciar un nuevo elemento llamo al constructor mediante

void* operator new (std::size_t size, void* ptr) throw();

Esta versión del operador new no reserva memoria. Simplemente devuelve el puntero ptr. La manera de construir una instancia de la clase TElement en la memoria ya reservada apuntada por memory_ptr sería

*memory_ptr = * new ((TElement*) memory_ptr) TElement(*source_ptr);

Source_ptr podría ser otra instancia de la misma clase de donde copiar – es decir, la construcción arriba llamaría al constructor de copia. Los parámetros de new son tan variables como los parámetros de los constructores de la clase.

Para destruir un elemento sin liberar memoria llamo explícitamente a su destructor.


(MiClase* ptr)->~MiClase();

Reservar memoria en el stack

La forma más rápida es reservar memoria en el stack en lugar del heap. Esta reserva sucede cada vez que declaro una variable dentro de una función. Si sé que mi cadena de texto no tendrá más que mil bytes, entonces


char a[1001];

consiste en incrementar el puntero del stack, mientras


char* b = new char[1001];

en la búsqueda de un hueco lo suficientemente grande en el heap. La primera manera es muchísimo más rápida que la segunda. Teniendo en cuenta que la reserva dinámica puede, además, introducir errores de programación por olvidarse de liberar la memoria, es casi siempre preferible a ser generoso con la memoria RAM. Los ordenadores de hoy en día tienen de sobra.

Esta reserva en el stack se puede combinar con una gestión individualizada de la memoria. Dentro de mi clase puedo tener un miembro (posiblemente estático)


private: char _memory[1000];

Puedo convertirlo a un objeto de tipo TElement en la forma

TElement* element_ptr(void)
{
    return reinterpret_cast<TElement>(static_cast<char*>(_memory));
}

El static_cast no es obligatorio. Por la complejidad de la construcción conviene encapsularla en un método privado que convierte el almacén de memoria al tipo de clase requerido.

Hay implementaciones de la STL que usan esta técnica. Por ejemplo, la clase std::string contiene un búfer pequeño como miembro de la clase. Esto hace el almacenamiento de cadenas de textos cortos rápido. Si la longitud del string excede el tamaño del búfer, entonces se pide memoria dinámica.

Allocators

Los contenedores de la STL suministran otro mecanismo de cambiar la gestión de memoria mediante una clase allocator. Este allocator aparece como un parámetro en las plantillas de los contenedores. Aunque es una forma sofisticada de separar la gestión de memoria del funcionamiento del contenedor, aporta un problema nuevo: un contenedor con un allocator diferente no tiene el mismo tipo que un contendor con el allocator por defecto. Por lo tanto, dos instancias del mismo contenedor se comportan como dos contenedores distintos.

std::list<int, MiAllocator> miLista;
std::list<int> listaNormal;
listaNormal = miLista // Error: listaNormal y miLista no son de la misma clase

Sin embargo, los contenedores pueden interactuar al nivel de iteradores.

// Esto está permitido.
listaNormal.insert(listaNormal.begin(), miLista.begin(), miLista.end());

Si estás pensando en utilizar allocators personalizados, entonces considerar obligar el uso del contenedor modificado por toda la aplicación, por ejemplo utilizando un typedef que define un tipo de lista propia derivada de la STL.

Conclusión

Hemos visto varias formas de como hacer más rápida la creación de memoria dinámica. El incremento en velocidad se suele pagar por un uso menos óptimo de la memoria. Sin embargo, en la mayoría de los casos, no falta memoria. La gestión individual de la memoria es una tarea compleja y requiere cuidado. Un mal algoritmo para guardar, qué elementos reservados están en uso y cuales no, puede fácilmente acabar con la ventaja, que la administración propia de la memoria podría traer.

En C++ se puede separar la reserva de memoria de la construcción de instancias. La biblioteca STL permite utilizar gestores de memorias propias. Es una herramienta potente pero que puede traer complicaciones a la hora de mezclarla con contenedores con el gestor estándar.

Referencias

Escribe tu dirección de correo electrónico para suscribirte a este blog, y recibir notificaciones de nuevos mensajes por correo.

Únete a otros 61 suscriptores

Archivos

May 2024
L M X J V S D
 12345
6789101112
13141516171819
20212223242526
2728293031