• No se han encontrado resultados

C++/CLI: Bloqueos y punteros interiores

N/A
N/A
Protected

Academic year: 2021

Share "C++/CLI: Bloqueos y punteros interiores"

Copied!
6
0
0

Texto completo

(1)

C++/CLI: Bloqueos y punteros interiores Bloqueos

El lector habitual de este blog debe estar ya acostumbrado a leer aquí sobre el montículo manejado y el hecho de que las referencias en .NET son punteros móviles, es decir, apuntan y siguen al objeto en cuestión a lo largo de todos sus movimientos dentro del citado montículo, ya que uno de los elementos más potentes del .NET es la compactación de objetos asignados y la recolección de los que se han liberado, ya sea de forma automática al salir de ámbito o bien mediante su marcado manual para liberación (uso de delete en C++/CLI y de Dispose() en C#). Pues bien, en esta entrada vamos a hablar sobre cómo bloquear un elemento dentro de dicho montículo y cómo acceder mediante un puntero a su interior.

Una referencia a un objeto no es más que una variable almacenada en la pila y que en su interior contiene los datos suficientes y necesarios para que el sistema de ejecución del .NET pueda actualizar la posición que el propio objeto ocupa dentro del montículo manejado. Por ejemplo, la sentencia

Object ^o=gcnew Object();

reserva en la pila manejada y en el contexto actual un bloque de bytes con la etiqueta “o” que a su vez contiene un puntero a otro bloque de memoria situado en el montículo manejado, y que se corresponde a la representación física de lo que quiera que sea un Object.

Cuando ejecutamos la línea o->ToString();

el compilador implementa una serie de instrucciones que miran en “o” dónde está realmente el objeto en el montículo manejado y ejecutan el método ToString() sobre dicho objeto. De igual forma, cuando el método o función en donde se ha declarado “o” termina su

ejecución, “o” deja de existir, pero no así el bloque dentro del montículo manejado. Lo que en un lenguaje como C++ sería una fuga de memoria muy seria, en C++/CLI y dentro del universo .NET no es más que un paso perfectamente válido para que, al dejar huérfano lo que venía representado por “o”, quede marcado para su futura destrucción cuando se produzca una pasada del recolector de basura.

Supongamos ahora que seguimos con nuestro objeto dentro de ámbito y que se ha producido una pasada de compactación de memoria. ¿Qué ha ocurrido? Pues muy posiblemente, que el objeto situado en el montículo haya sido desplazado a otra parte, y a su vez se haya

actualizado la estructura “o” guardada en la pila para que apunte a la nueva localización. Pero quizás nos interese bloquear la posición que el objeto ocupa en el montículo, es decir, fijar su dirección física para, por ejemplo, pasar dicha dirección a una función de código nativo, que por supuesto no entiende ni de recolectores de basura ni de movimientos no controlados de memoria ni de nada de eso. De momento, el único lenguaje que permite esto es C++/CLI

(2)

mediante el uso de pin_ptr. La forma en la que otros lenguajes .NET permiten hacer algo parecido es mediante el Interop por atributos, pero desde luego no de forma tan directa. Veámoslo con un ejemplo sencillo.

#include "stdafx.h"

using namespace System;

#pragma unmanaged

double nativoMedia(int *p,int tam)

{

int suma=0;

for(int i=0;i<tam;i++) suma+=p[i];

return suma/tam;

}

#pragma managed

ref class Mates

{

array<int>^m_numeros;

public:

void Incializa(int cantidad) {

m_numeros=gcnew array<int>(cantidad); for(int i=0;i<cantidad;i++)

m_numeros[i]=i; }

property array<int>^Array {

array<int>^get(void) {return m_numeros;} }

};

int main(array<System::String ^> ^args) {

Mates ^mates=gcnew Mates; mates->Incializa(100);

pin_ptr<int> p=&mates->Array[0]; int *pp=p;

Console::WriteLine("La media {0}",nativoMedia(pp,100)); Console::ReadKey();

return 0;

}

El primer bloque de código es una función de código nativo que realiza la media de un array de enteros. Para asegurarnos de que es una función nativa, la englobamos mediante las dos directivas #pragma. Esto no es necesario cuando estamos construyendo código para producción, ya que el compilador se encargará de hacerlo de la mejor manera posible, pero para nuestro ejemplo debemos asegurarnos de que dicha función es completamente nativa. Es de observar que dentro de la función, estamos usando un puntero como un array. Si queremos hacerlo con sintaxis de punteros, una forma de código podría ser:

(3)

double nativoMedia(int *p,int tam) {

int suma=0;

for(int i=0;i<tam;i++) suma+=*p++;

return suma/tam;

}

Luego definimos una clase que contiene un array manejado de enteros y que posee un método público que se encarga de llenar el array con una serie de valores consecutivos. Lo siguiente es una propiedad que nos devuelve una referencia al propio array interno, lo que en un código de producción no es una buena idea (la de exponer las interioridades de una clase), pero para nuestro ejemplo nos sirve perfectamente.

Ya en main() creamos un objeto del tipo Mates y lo inicializamos con los 100 primeros números. Y entonces viene lo interesante. La línea

pin_ptr<int> p=&mates->Array[0];

está declarando un elemento p que es un puntero bloqueado al primer valor del array interno de la clase Mates. Lo que internamente hace es marcar el array m_numeros como bloqueado e inamovible, y luego devuelve la dirección real del primer elemento dentro del montículo manejado. Esto requiere la explicación de que los arrays .NET están almacenados de forma contigua dentro del montículo manejado, es decir, cuando nosotros creamos un array manejado, el sistema reserva un bloque de memoria manejada capaz de contener todo el array de una sola vez (de ahí que si necesitamos cambiar su tamaño, debemos crear un nuevo array con el nuevo tamaño y copiar el antiguo sobre el nuevo). Esto es así por definición. Pero un pin_ptr<int> no es un puntero nativo a un entero, sino que se trata de un tipo nuevo que almacena una dirección que apunta a algo bloqueado dentro del montículo manejado. Para convertirlo a un int* debemos asignarlo a una variable de este tipo, que es lo que hacemos en la siguiente línea. El resto es trivial.

Pero si esto fuera código de producción habríamos cometido un error bastante serio. ¿Se da alguien cuenta de cuál es? Que el lector piense antes de seguir leyendo.

El error consiste en que hemos dejado bloqueado un elemento dentro del montículo

manejado y sólo se libera a la hora de cerrar el programa, que en nuestro caso da igual, pero en otras situaciones podría tener serias consecuencias de rendimiento y podría incluso llegar a impedir la asignación de nuevos bloques de memoria dentro del montículo manejado. El motivo es muy sencillo: el compactador, asignador y liberador de memoria del montículo manejado ha de tener completa libertad a la hora de poder mover todo lo que haya en él. El autor ignora si posee algún tipo de optimización para lidiar con elementos bloqueados (imagina que sí), pero desde luego no será tan potente como el asignador nativo (que se usa cuando hacemos new o malloc), que es capaz de buscar el bloque más óptimo de entre todos los posibles –de ahí que la asignación de memoria en .NET sea algo más rápida que su

equivalente nativo en determinadas circunstancias.

La solución a este problema pasa por hacer algo como lo expuesto más abajo o por mantener dicho puntero bloqueado el menor tiempo posible.

(4)

int main(array<System::String ^> ^args) {

Mates ^mates=gcnew Mates; mates->Incializa(100); {

pin_ptr<int> p=&mates->Array[0]; int *pp=p;

Console::WriteLine("La media es {0}",nativoMedia(pp,100)); }

Console::ReadKey();

return 0;

}

NOTA: Al rodear un bloque de código mediante llaves dentro de una función, forzamos al compilador a que cree un nuevo contexto local, siendo equivalente a una llamada a función, de modo que p y pp son variables locales dentro de este bloque, y quedan liberadas en la llave de cierre, de forma que el bloqueo de nuestra variable apenas ha durado lo necesario.

Punteros interiores

¿Se puede optimizar el código de arriba de alguna forma? ¿Podemos evitar bloquear un objeto? La respuesta directa es que la situación comentada consiste en la mejor solución posible, pero dependiendo de circunstancias, podemos evitarnos el bloqueo de todo un objeto mediante el uso de punteros interiores, aunque esta vez no podamos salirnos del código manejado.

Retomemos el código anterior y modifiquémoslo un poco: #include "stdafx.h"

using namespace System;

ref class Mates

{

array<int>^m_numeros;

public:

void Incializa(int cantidad) {

m_numeros=gcnew array<int>(cantidad); for(int i=0;i<cantidad;i++)

m_numeros[i]=i; }

property array<int>^Array {

array<int>^get(void) {return m_numeros;} }

};

int main(array<System::String ^> ^args) {

Mates ^mates=gcnew Mates; mates->Incializa(10);

for(int i=0;i<10;i++) Console::Write("{0} ",mates->Array[i]); Console::WriteLine("");

(5)

*medio=99;

for(int i=0;i<10;i++) Console::Write("{0} ",mates->Array[i]); Console::ReadKey();

return 0;

}

La clase Mates es la misma, pero el interior de main() ha cambiado por completo. El código que nos interesa está en las líneas

interior_ptr<int>medio=&mates->Array[5]; *medio=99;

que nos muestra cómo obtener un puntero interior. Aunque el ejemplo no demuestra ninguna utilidad, podemos observar cómo se hace, pero tiene una serie de ventajas que pasamos a explicar.

Un puntero interior permite semántica de punteros para objetos de tipo .NET siempre y cuando no nos salgamos del interior de un objeto (de ahí su nombre). Y tienen la ventaja de que son objetos manejados –es decir, siguen siendo móviles y no afectan para nada al recolector ni al asignador-.

Un puntero interior se puede aplicar a cualquier elemento situado dentro del montículo manejado, aunque tiene ciertas limitaciones: no puede ser miembro de una clase, sólo se puede declarar en la pila y no se puede obtener su dirección, es decir, no podemos obtener un puntero interior de un puntero interior, y tampoco se puede pasar como puntero a un bloque de código nativo.

¿Entonces, para qué me sirve? Buena pregunta. Sigamos.

Podemos obtener un puntero interior a un elemento de un array, bloquearlo conforme hemos visto en la sección anterior a través de un pin_ptr y pasarlo a un bloque de código nativo. Como en este caso estamos bloqueando una parte de un objeto secuencial, realmente estamos bloqueando el objeto completo, por lo que poco o nada hemos ganado respecto a lo de antes. ¿Entonces qué?

En primer lugar, si dicho objeto fuera un elemento agregado que contuviera referencias a otros elementos del montículo, al obtener un puntero interior a un elemento agregado y fijándolo después, sólo bloquearíamos el objeto secundario, no el primario, mejorando y optimizando en cierta medida la cantidad de memoria no móvil.

Y en segundo lugar, imaginemos que tenemos una biblioteca o un código fuente que funciona a las mil maravillas con C++ y que trabaja, para desgracia nuestra, con punteros normales. Si disponemos del código fuente, podemos adaptar su funcionamiento sólo cambiando la firma de los métodos sin necesidad de tocar nada del cuerpo de los mismos. Tomando el ejemplo anterior, sólo tenemos que cambiar la firma del método

double nativoMedia(int *p,int tam)

(6)

double nativoMedia(interior_ptr<int> p,int tam)

y dejar el cuerpo del mismo sin tocar. De este modo nos evitamos modificar lo que realmente resulta peligroso dentro del código fuente: los algoritmos y las implementaciones.

Ya para terminar, pongamos el ejemplo completo: #include "stdafx.h"

using namespace System;

double nativoMedia(interior_ptr<int> p,int tam) {

int suma=0;

for(int i=0;i<tam;i++) suma+=*p++;

return suma/tam;

}

ref class Mates

{

array<int>^m_numeros;

public:

void Incializa(int cantidad) {

m_numeros=gcnew array<int>(cantidad); for(int i=0;i<cantidad;i++)

m_numeros[i]=i; }

property array<int>^Array {

array<int>^get(void) {return m_numeros;} }

};

int main(array<System::String ^> ^args) {

Mates ^mates=gcnew Mates; mates->Incializa(100);

interior_ptr<int>p=&mates->Array[0]; Console::WriteLine(nativoMedia(p,100)); Console::ReadKey();

return 0;

Referencias

Documento similar

Entre ellos, el diseño de interiores de casas contemporáneas puede ser el diseño que mejor puede deshacerse del concepto limitado de un cierto estilo, porque acoge las

Los datos personales facilitados en estas solicitudes, en aplicación del artículo 11 de la Ley Orgánica 3/2018, de 5 de diciembre, de Protección de Datos Personales

Como otros AINE, los productos que contengan naproxeno deben utilizarse con precaución en los pacientes con disfunción renal o antecedentes de renopatía, ya que el naproxeno inhibe

Como señalan Deen (1985) y Ponte (1989), entre otros autores, para cumplir, no sólo el objetivo preventivo sino también el de soporte, la tutoría no puede ser un elemento aislado

Se tuvo en cuenta los trabajos de grado de nuestras compañeras de enfermería Laura Rubiano-Laura Orjuela quienes hicieron un video educativo sobre castigo físico

En general, este rol puede realizar cualquier modificación y puede existir más de uno dentro de la plataforma Profesor: puede crear, modificar y borrar actividades o recursos

- Resolución de 30 de agosto de 2018, de la dirección general de Formación Profesional y Enseñanzas de Régimen Especial, de la Conselleria de Educación, Investigación, Cultura

Se enganchan a unos insertos (clips) laterales del casco, que debe estar preparado para su utilización con el HANS.. INSERTOS