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

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 una variable 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 ParamClase>
class UnaClase
{
public:
    // Declaración de un patrón de método
    template <class ParamMétodo>
    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<int>::*TIPO_TEMPLATE)(float&);
TIPO_TEMPLATE puntero_a_método_template =
    &UnaClase<int>::método_template<float>;

// Llamar a este método requiere un objeto, ya que
// el método no es estático.
UnaClase<int> 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

Con “guardianes de alcance” me refiero a objetos locales en una función que tienen algún efecto sólo por existir. Sólo tienen un constructor y un destructor, que suelen reservar y liberar algún recurso.

El mutex locker

Una tarea típica en la programación multi-hilo es usar un mutex. El hilo que reserva el mutex puede seguir; los demás hilos que quieren reservarlo deben esperar. En el código, esto suele aparacer así:

// Definición del mutex
Mutex mi_mutex;

// Una función que usa el mutex
void mi_funcion
{
    // Reserva el mutex o espera hasta que pueda reservarlo
    mi_mutex.lock();

    // Haz algo

    // Libera el mutex
    mi_mutex.unlock();
}

Este emparejamiento de lock y unlock aparece frecuentemente en un programa multi-hilo. No obstante, el unlock puede ser necesario en varios puntos, cuando una función tiene varias salidas – por ejemplo por un return en medio o por un lanzamiento de una excepción. Poner un unlock en cada rama engorda la función y es fácil olvidarse de algúno. Pero olvidarse de liberar algún recursos puede dar a errores difíciles de localizar.

Por suerte existe una manera de no olvidarse de un unlock. Esto se puede conseguir mediante una clase MutexLocker. El constructor de esta clase reserva el mutex y el destructor lo libera.

class MutexLocker
{
    // El constructor reserva el mutex
    MutexLocker(Mutex& mutex_a_reservar)
    : _mutex_reservado(mutex_a_reservar)
    {
        _mutex_reservado.lock();
    }
    
    // El destructor libera el mutex
    ~MutexLocker(void)
    {
        _mutex_reservado.unlock();
    }
    
    // La REFERNCIA al mutex que se quiere reservar
    Mutex& _mutex_reservado;
};

El código arriba se modifica así

// Definición del mutex - esto no se modifica
Mutex mi_mutex;

// El uso, sí, se modifica
void mi_funcion
{
    const MutexLocker mi_mutex_reservado(mi_mutex)
    // Haz algo
}

Como vemos, sólo hace falta una línea en la función. Cuando la variable mi_mutex_reservado sale fuera del alcance, se llama al destructor que libera el mutex. Esto es así, no importa por qué salida la función termina. De esta forma tenemos un “guardián de alcance”. Nos limpia todo al salir del alcance de la función.

En general, se puede declarar const a un guardián, ya que no lo modificamos durante su vida. Ni siquiera podríamos, ya que un guardián de alcance no tiene métodos. Sólo tiene un constructor y un destructor.

Por hacerlo académicamente aún más correcto podríamos impedir hacer copias de la clase MutexLocker para evitar, que una instancia afecte al mutex controlado por otra instancia. Esto podemos conseguir declarando el constructor de copia y el operador de asignación como métodos privados y sin implementarlos. De esta forma conseguiríamos un error de compilación si alguien intentara copiar una instancia de la clase MutexLocker. No obstante, es un problema académico. Normalmente nadie intenta copiar un guardián de alcance.

Trazas de entrada y salida en una función

Los guardianes de alcance se puede utilizar para más tareas. Por ejemplo, un objeto que escribe una traza cuando entramos en la función y otra cuando cuando salimos.

class TraceWriter
{
    // El constructor escribe la traza de entrada
    TraceWriter(const char* nombre_de_funcion)
    : _funcion(nombre_de_funcion)
    {
        std::cout << "Entro en " << _funcion << std::endl;
    }
    
    // El destructor escribe la traza de salida
    ~MutexLocker(void)
    {
        std::cout << "Salgo en " << _funcion << std::endl;
    }
    
    // El nombre de la función. Puede ser const, ya que no
    // se modifica durante la vida del objeto.
    const std::string _funcion;
};

// La macro que crea un objeto de la clase TraceWriter
// (si el compilador soporta la macro __FUNCTION__)
#define LOG_FUNCION  \
    TraceWriter _trace_writer(__FUNCTION__)

En una función, se llama a la macro en la primera línea

void mi_funcion
{
    LOG_FUNCION();
    // Haz algo
}

Así obtenemos una traza en stdout cuando entramos en la función y otra cuando salimos por no importe qué salida. Como usamos una macro, la podemos definir como nada en una versión de release.

Con un miembro estático en la clase TraceWriter es posible crear anidamientos. El constructor aumenta el nivel de anidamiento y el desctructor lo disminuye. Para cada nivel de anidamiento se imprime alguna cantidad de espacios delante la traza.

Memoria reservada

No olvidarse de liberar la memoria reservada es importante. Se puede utilizar la técnica descrita para reservar y liberar memoria dinámica en una función. En la biblioteca estándar de C++ ya hay clases que implementan este comportamiento:

  • unique_ptr para el estándar C++11 y
  • auto_ptr para el estándar C++0x

Presentamos aquí el uso de unique_ptr. No obstante, el código sería igual sustituyendo unique_ptr por auto_ptr.

void mi_funcion
{
    // Reserva memoria con new
    std::unique_ptr<MiClase> puntero_a_mi_objeto(new MiClase);
    
    // Haz algo con puntero_a_mi_objeto
    
    // La memoria se libera al salir de la función
}

Cuando se sale de la función, el destructor de unique_ptr llama delete puntero_a_mi_objeto. Con esto libera la memoria cuando se sale de la función.

Conclusión

Hemos visto que los “guardianes de alcance” sirven para asegurar la liberación de recursos. Además, pueden reservar y liberar estos recursos con una sóla línea en la función que los utiliza. La técnica consiste en crear una clase, cuyo constructor reserva el recurso y cuyo destructor lo libera. Esta clase suele guardar una referencia a este recurso.

Este concepto sólo se puede utilizar con los lenguajes que llaman al destructor inmediatamente después que un objeto sale del alcance. Por eso no se puede usar con lenguajes que usan un colector de basura como Java, ya que en estos lenguajes no está definido, cuando se llama al destructor. Por lo tanto no está asegurado que se libera un recurso a tiempo.

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

El operador de igualdad se usa para dos conceptos diferentes en los lenguajes de programación: la identidad y la equivalencia. En este artículo quiero aclarar estos conceptos y dar ejemplos como algunos lenguajes de programación los implementan.

Los conceptos

Habitualmente se denomina igualdad al operador == (o = en algunos lenguajes como Pascal o Visual Basic). Pero hay dos conceptos de lo que puede ser igual: la entidad que contiene un valor (identidad) o el valor que contiene (equivalencia).

Decimos que las dos variables a = 5 y b = 5 son iguales en sentido matemático porque contienen el mismo valor 5. Sin embargo, a y b son dos variables distintas en sentido informático si se guardan en dos direcciones distintas en la memoria. En el último caso hablamos de identidad: las variables a y b son iguales cuando hacen referencia a la misma entidad. En el primer caso hablamos de equivalencia. Las variables pueden hacer referencia a una memoria diferente, pero contienen el mismo valor.

Es obvio que la identidad es un criterio más estricto. Dos objetos distintos pueden tener el mismo valor o no, pero el mismo objeto comparado a si mismo siempre es igual.

El uso en los lenguajes

Implementación de identidad y equivalencia
Significado de igualdad
Equivalencia
(mismo valor)
Identidad
(mismo objeto)
Lenguajes compiladas (“antiguas”) Operador == Operador == entre punteros
Lenguajes interpretadas (“modernas”) Método equals Operador ==

Los lenguajes de programación más antiguas como C o Fortran suelen interpretar la igualdad como equivalencia. Para esto puede haber varias razones. Primero la equivalencia es el concepto más matemático (Fortran viene de formula translation). Luego no existía el concepto de objeto cuando se crearon estos lenguajes. Los tipos de variables eran los básicos como caracteres, números enteros y números de coma flotante. Los compiladores convertieron las expresiones a == b a una comparación entre los valores de dos direcciones de memoria. Para comprobar identidad se podían comparar los punteros a estas memorias. No extraña que se sobrecarga el operador == en C++ con un significado de equivalencia hasta hoy en día.

El asunto se cambió con los lenguajes orientado a objetos en que no existen punteros como C#, Java o PHP. En estos lenguajes, no son las variables que guardan los objetos sino el intérprete. La variable de objeto guarda meramente un identificador. Por lo tanto, una comparación entre dos variables es una comparación entre identificadores y no valores. En consecuencia la igualdad tiene la conotación de identidad.

También cabe señalar que estos lenguajes no permiten sobrecarga el operador == como en C++. No es posible crear un operador == que compare el contenido. Tampoco conviene que el intérprete realice una comparación binaria entre dos objetos. La representación binaria depende de la plataforma y un lenguaje como Java se creó justamente para no depender de ella.

Clases inmutables

Ninguna regla sin excepción. En Java o C#, sí, se pueden comparar los valores de dos strings con el operador ==. Esto es posible ya que estas clases son inmutables (o constantes según la jerga de C++): Sus instancias ya no se pueden modificar una vez creadas. Los intérpretes – la máquina virtual de Java o el framework de .NET, respectivamente – aprovechan esta característica. Si initializo una instancia de string con un valor ya existente en otra instancia, entonces no se crea un nuevo valor, sino la nueva instancia apunta al valor existente. Por esto, la nueva variable contiene una referencia a la misma instancia que la variable antigua. Ambas apuntan al mismo objeto y, por lo tanto, son idénticos.

Las clases inmutables tienen más características interesantes. Por ejemplo, no hace falta copiar sus contenidos. En una copia profunda de una clase (deep copy en inglés) debo crear constructores de copia para cada subobjeto de una clase, pero no para un string. La copia causa que una variable de clase string apunta a otro objeto guardado en el intéprete, pero el valor del objeto en si no se modifica ya que es inmutable. Operaciones sobre cadenas de texto reasignan referencias, pero no tiene por qué crear nuevas cadenas.

Conclusión

Hemos visto que existen dos conceptos de igualdad: la identidad y la equivalencia. La equivalencia es el concepto implementado en los lenguajes más tradicionales, mientras la identidad se ha impuesto en los lenguajes más recientes – entre otros motivos por razones técnicas. Los lenguajes que implementan el operador == como equivalencia suelen comparar identidades mediante punteros. Los lenguajes que implementan la identidad para el operador == suelen propicionar un método equals para la equivalencia.

Enlaces de interés

En este artículo quiero proponer una manera eficaz y general de suprimir una advertencia de “parámetro sin referencia” en los programas de C++. Tras explicar varias soluciones estándar, propongo la solución de la función fantasma.

Introducción

Si uno quiere programar con buen estilo, entonces suele activar el máximo nivel de advertencia del compilador. Esto, por cierto, también es una ayuda para entender bien el concepto de lenguaje. Dejarse advertir sobre todo puede resultar en que no todas las advertencias aciertan. Sin embargo, desactivarlas a coste de no ser advertido por errores significativos tampoco conviene.

Por ejemplo, si compilamos con Microsoft Visual Studio el constructor de copia

Object(const Object& source) {}

entonces se obtiene la siguiente advertencia: warning C4100: ‘source’ : parámetro formal sin referencia. Esto sería un caso típico de un constructor de copia que realmente no copia nada. Debemos mantener la interfaz para que no deje de ser un constructor de copia, pero actualmente no usamos el parámetro source.

Eliminar el parámetro

El artículo Visual C++: Warning C4100 discuta varias opciones que mencionamos aquí

  • Hacer caso a la advertencia y eliminar el parámetro no usado.
  • Si necesitamos el parámetro pero estamos seguros que no lo usaremos, entonces podemos quitar el nombre. Un constructor de copiar se quedaría sin “source”

    Object(const Object&) {}
    
  • Si queremos mantener el nombre aunque actualmente no lo usamos, podemos comentarlo.

    Object(const Object& /* source */) {}
    

    Debemos tener en cuenta que el comando @param de un comentario de documentación como lo usuaría Doxygen se quedría obsoleto. Deberíamos comentar el comantario. Además, los comentarios /* */ no suelen poder anidarse – un problema a la hora de deshabilitar bloques de código.

  • Deshabilitar la advertencia. En Microsoft Visual Studio esto se puede conseguir de la siguiente manera:

    #pragma warning( push )
    #pragma warning( disable: 4100 )
    Object(const Object& source) {}
    #pragma warning( pop )
    

    El problema de este enfoque es que se necesita una directiva #pragma diferente para cada compilador y ni siquiera está garantizado que haya. Así no conviene si el programa debe compilar en varias plataformas.

Una macro de “parámetro no referenciado”

A parte de estas propuestas suele haber otro apaño de usar el parámetro. Por ejemplo

Object(const Object& source)
{
    source = source;
}

Por ser una construcción no especialmente obvia, se puede crear una macro UNREFERENCED_PARAMETER.

#define UNREFERENCED_PARAMETER(param) param = param
Object(const Object& source)
{
    UNREFERENCED_PARAMETER(source);
}

La macro trae la ventaja que se puede definir de forma diferente según el efecto deseado. Una versión de release podría expandir la macro a nada:

#ifdef _DEBUG
#define UNREFERENCED_PARAMETER(param) param = param
#else
#define UNREFERENCED_PARAMETER(param)
#endif

Lógicamente volvería salir la advertencia cuando la macro suprime el uso del parámetro.

La desventaja de la asignación es tiempo de cálculo que requiere y que no está disponible en todas las clases. Estas clases son qquellas que tienen el operador = sobrecargado, pero ninguna sobrecarga admite una instancia de la clase misma como parámetro (o fuente) de la asignación.

Función fantasma

Mi forma preferida es una mezcla de varias propuestas y usa una función fantásma.

// La función fantásma (dummy function)
inline void reference_parameter(const void*) { }

// La macro
#define UNREFERENCED_PARAMETER(param) \
    reference_parameter(static_cast<const void*>((&param)));

// El uso
Object(const Object& source)
{
    UNREFERENCED_PARAMETER(source);
}

Esta propuesta reune todas las ventajas.

  • Gracias a referenciar el parámetro no referenciado por una macro, podemos eliminar y cambiar esta definición sin tocar el resto del código.
  • Si dejamos la macro como aquí propuesta, entonces se reemplaza por una llamada a una función con un puntero al parámetro no usado. Esta operación siempre es posible con cualquier objeto.
  • La función reference_parameter no pone nombre al parámetro, por lo cual no aparece una advertencia de “parámetro sin referencia”.
  • Finalmente es una operación rápida. Copiar un puntero es rápido y como la función no hace nada, desaparecerá con la expansión inline.

Referencias

Enlaces externos
En este sitio

Lectura adicional


Durante la modificación de código conviene a veces desactivar partes del código. La forma habitual es usar comentarios de C /* */. Esto puede traer problemas, porque estos comentarios no se puede anidar.

Una buena manera para evitar este problema es no utilizar los marcadores de comentarios /* */. Utiliza comentarios de línea simple //. La herramienta de documentación automatizada Doxygen permite marcar comentarios de documentación con una triple barra ///.

La forma más segura de desactivar código es mediante macros de compilación. Cualquier cosa que se encuentre entre #if 0 y #endif no se compila. Los entornos de desarrollo integrados modernos son capaces de marcar código excluído de la compilación con un color de fuente diferente para distinguirlo.

En lugar de #if 0 puede escribir también #if NOMBRE_DEFINIDO. Para desactivaciones no tan temporales queda más claro que un simple 0, ya que el NOMBRE_DEFINIDO puede conllevar la razón por qué se desactivó el código. Además, se puede definir este símbolo mediante una opción en la línea de comando del compilador (normalmente la opción /d) sin modificar el código.

La ventaja de las directivas respecto a los comentarios /* */ es su capacidad de anidamiento. Podemos desactivar código aunque ya contiene un trozo desactivado. (No olvides marcar con un comentarios el #endif correspondiente a cada #if)

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

A la primera vista uno diría que no es posible programar orientado a objetos con el lenguaje de programación C porque no tiene clases. Esto es parcialmente correcto, ya que clases son un componente importante para programar orientado a objetos. El lenguaje C, en cambio, fue diseñado para la programación estructurada. Sin embargo, el lenguaje C permite definir objetos.

Programar orientado a objetos es un paradigma, es decir, una forma de diseñar código. Hay lenguajes como C++ o Java que están diseñados para este paradigma, pero no se programa orientado a objetos meramente por utilizar a uno de estos lenguajes. Por ejemplo, si todos los miembros de una clase son públicos y estáticos, entonces equivalen a variables y funciones globales con el prefijo “NombreDeClase::”. (Prefijos pueden reemplazar espacio de nombres.)

En cambio, se puede aplicar el paradigma de orientado a objetos a lenguajes de programación no diseñados para esto, aunque será lógicamente con complicaciones. Como resolver estas complicaciones para el lenguaje de programación C es el tema de este artículo.

Definición de clases

Los datos de una clase se pueden guardar en una estructura. De hecho, en C++ un struct no es otra cosa que una clase con todos los miembros públicos. Como C no permite métodos, usamos funciones globales. El equivalente de C++

class MiClase
{
public:
	int miDato;
	void hazAlgo(void)
	{
	    miDato = 5;
	}
};

en C sería

struct MiClase
{
	int miDato;
}

void hazAlgo(MiClase *const miInstancia)
{
    miInstancia->miDato = 5;
}

Nótese que el primer parámetro de la función en C tiene el mismo tipo que el puntero this en C++.

El uso de estas clases sería en C++

MiClase miInstancia;
miInstancia->hazAlgo();

y en C

MiClase miInstancia;
MiClase_hazAlgo(&miInstancia);

Mediante punteros a funciones y macros es posible que se puede construir el aspecto de llamadas a métodos de C++ como en

miInstancia->miMetodo();

Sin embargo, no lo vamos a discutir en este artículo. Se trata meramente de un aspecto estético que, en mi opinión, más confunde que aclara. El lenguaje C no es C++ y programación orientado a objetos no deja de serlo sólo por ordenar de otra forma los componentes de una llamada a una función.

Estructuras opacas

Es posible declarar y utilizar un tipo de puntero a una estructura sin definir la estructura misma. Con este mecanismo se pueden crear estructuras opacas.

En el fichero de cabecera (el .h) defino un tipo de puntero a una estructura

typedef struct MiStruct* MiClase;

La definición de la estructura estará dentro de un fichero de implementación (*.c).

struct MiStruct
{
    int miDato;
};

El interfaz en el fichero de cabecera define un puntero, que es un tipo de un tamaño conocido. Por eso el compilador puede compilar un programa que usa un puntero aunque el tamaño del elemento a que apunta es desconocido. El usuario de MiClase accede a la biblioteca meramente con el puntero.

Este encapsulamiento de datos es incluso mejor que el de C++ ya se define la estructura de datos en el fichero de definición. Este fichero no tiene por qué ser visible para el usuario de la estructura opaca, ya que se puede meramente publicar los ficheros binarios correspondientes. En otras palabras, se puede esconder por completo la estructura de datos. Esto no es posible con una clase en C++, ya que la declaración de la clase en el fichero de cabecera debe contenedor todos los campos de datos.

Herencia

No existe un mecanismo de clases derivadas en C, sin embargo esto se puede simular. El método de una clase/estructura, que hereda de otra, puede convertir el puntero de su estructura a una estructura de la clase base y llamar con ello los métodos de esta. Esto corresponde a una herencia privada. Para esto hace falta que la primera parte de la estructura derivada contiene en el inicio los mismos campos como la estructura base.

Para hacer una herencia pública, el usuario debe convertir el puntero en su llamada.

MiClaseDerivada miInstancia;
MiClaseBase_HazAlgo((MiClaseBase) miInstancia);

Obviamente esta construcción es aún menos práctica ya que es el usuario debe pensar si llama a una función de la clase base o de la clase derivada.

La herencia complica más en el lenguaje C que ayuda. Por esto vale la pena tener en cuenta una alternativa: usar una clase en lugar de heredar de ella.

Identificadores en lugar de punteros

Una manera potencialmente más potente para la programación orientado a objetos es abstraer de los punteros y referenciar objetos nada más que por un identificador numérico. Cada método de una clase debe primero comprobar si el identificador numérico corresponde a su clase correspondiente. Esto es más complejo pero una buena alternativa cuando se quieren manejar muchas clases diferentes en C. De hecho, muchos lenguajes modernos como Java, C# o PHP devuelven identificadores y no punteros a los objetos, lo cual les permite reordenador los objetos en la memoria si hace falta. Sólo el gestor de objetos sabe bajo qué dirección de memoria se encuentra qué objeto.

Un camino intermedio y buen compromiso es exportar un tipo de identificador diferente para cada clase. Entonces la clase gestiona nada más que sus propias instancias.

Resumen

Hemos visto que se pueden crear “clases” y “objetos” con el lenguaje de programación C. Referenciar objetos por punteros a estructuras opacas o mediante identificadores es una manera potente para el encapsular datos.

Por no ser un lenguaje diseñado para la programación orientado a objetos, el uso de este parádigma a veces no queda muy elegante en el código y tampoco es posible aprovechar la potencia de la herencia sin trucos adicionales.

No obstante, la programación orientado a objetos es posible con el lenguaje C y debería tenerse en cuenta a la hora de realizar proyectos grandes. De hecho hay unas bibliotecas que se han creado de esta forma como el servidor X o la biblioteca GObject.

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 48 seguidores

Archivos

junio 2017
L M X J V S D
« Ene    
 1234
567891011
12131415161718
19202122232425
2627282930