Stratos: Punto de Encuentro de Desarrolladores

¡Bienvenido a Stratos!

Acceder

Foros





Duda tonta C++ (vinculacion dinamica)

Iniciado por swapd0, 11 de Marzo de 2011, 11:02:16 PM

« anterior - próximo »

swapd0

Si tengo un objeto en una jerarquia de clases, ¿hay alguna forma de hacer que ese objeto funcione como si fuera de la clase padre?

En el codigo de abajo, siempre imprime "B" porque el objeto y apunta a un objeto de tipo B, hay alguna forma de hacer que imprima "A" pero sin crear un objeto nuevo (sin hacer algo como A *z=new A(x) y despues z->foo())

#include <iostream>

class A
{
public:

virtual void foo()
{
std::cout << "A" << std::endl;
}
};

class B : public A
{
public:

virtual void foo()
{
std::cout << "B" << std::endl;
}
};

int main (int argc, char * const argv[])
{
A *x=new B();
A *y;

x->foo();
y=(A *)x; y->foo();
y=dynamic_cast<A *> (x); y->foo();
y=static_cast<A *> (x); y->foo();
y=reinterpret_cast<A *> (x); y->foo();

    return 0;
}

Zaelsius

Se haría así:

y->A::foo();

Por cierto la vinculación dinámica no tiene nada que ver con la pregunta.

swapd0

Cita de: Zaelsius en 12 de Marzo de 2011, 01:29:36 AM
Se haría así:

y->A::foo();

Por cierto la vinculación dinámica no tiene nada que ver con la pregunta.

El problema es que eso no me vale, ya que el metodo foo lo se llamaria desde una clase generica y esta no sabe si el objeto en cuestion esta heredado o no.

Imaginate que el metodo foo se encarga de guardar los datos a disco o mostrarte en pantalla las propiedades de un objeto. Yo creo un objeto de la clase B, pero al mismo tiempo quiero ver la representación como si fuera un objeto de la clase A y B.

Creo que es un fallo de mi "diseño" y lo pongo entre comillas porque ya sabéis como se trabaja, lo que un día esta bien mañana no vale una mierda y lo tienes que rehacer.


fjfnaranjo

Lo más sencillo que se me ocurre, siendo un work-arrown de la ostia, es que te crees un método con un nombre diferente, y que sea este al que hagas referencia en la clase genérica. Ese método en A llamaría a foo y en B llamaría a A::foo. Y si no quieres mezclar foo el nombre mejor. Osea, el método "foo" lo defines en A y lo redefines en B. En A llama a cosoQueHaceA y en B cosoQueHaceB, y dentro de estos dos metes lo que tienen los foo actuales.

Ñapa alert \OFF
fjfnaranjo.com - Creating entertainment - Creando entretenimiento
fjfnaranjo [4t] gm4il [d0t] c0m (mail y msn)

fjfnaranjo

#4
Am perdón, y sólo por agregarlo e intentando no equivocarme.

La vinculación dinámica, tal como yo la conocía (no tengo estudios formales), es un mecanismo que permite que el programa se conecte (vincule) a otro programa (habitualmente un librería) en tiempo de ejecución o de carga del programa. Se supone que esa labor de enlace la realiza el sistema operativo y el linker de la aplicación sólo necesita un poco de información sobre las posibles librerías para incorporarla al ejecutable durante el enlazado. Pero sin embargo, vinculación dinámica parece que se conoce formalmente como otra cosa comentada más abajo, así que siga leyendo... :P

EDIT: Edito para aclarar la definición que pongo he indicar que más abajo se sigue discutiendo sobre ello.
fjfnaranjo.com - Creating entertainment - Creando entretenimiento
fjfnaranjo [4t] gm4il [d0t] c0m (mail y msn)

Zaelsius

Swap no termino de entender qué es lo que quieres hacer. ¿Lo que quieres es esto?:

class B : public A
{
public:

virtual void foo()
{
                A:foo();
std::cout << "B" << std::endl;
}
};



Mars Attacks

Yo no entiendo tu problema. Si sobrecargas foo en B es porque quieres que en B se comporte como en B y no como en A. Por tanto, hay un error de concepción si lo que quieres es que B se comporte "como B y como A". A menos que quieras que dentro de la sobrecarga de B haga su A::foo(), de forma que te imprima ambas.

En fin, es más fácil si explicas tu problema concreto para sugerir algún patrón más adecuado.

Wops, adelantada de Zaelsius.

Vicente

#7
Si no entiendo mal lo que quiere es hacer shadowing de un metodo. Es decir, que la sobreescritura en tiempo de ejecucion se resuelva por el tipo declarado de la variable y no por el tipo del objeto que contiene (algo asi como ligadura estatica vs ligadura dinamica vamos).

En C# se hace con new, pero estoy con Zaelsius y con Mars, ni idea porque quieres hacer esto. En C# tiene su utilidad cuando tienes una colision de nombres en una libreria de terceros que no puedes modificar, y algun otro caso rarisimo...

fjfnaranjo

En C++ no hay diferencia entre el tipo declarado de una variable y el tipo de lo que contenga esa variable, con la única excepción de los punteros a void, que están declarados como void* pero pueden contener cualquier puntero a cualquier tipo (int*, Pepito*, void**, etc.).

En programación orientada a objetos tal como se entiende en C++, se daría polimorfismo cuando una instancia de tipo X pudiese ser referenciada en otra parte como instancia de tipo Y por el hecho de ser la segunda una superclase de la primera. Pero cualquier referencia a un método de la clase Y que estuviese declarado en la clase X sería resuelto con la implementación de está última (este es el comportamiento por defecto que se espera de la aplicación según la OOP).

Así que lo único que se me ocurre, es que alguna variable vaya a ser un puntero, declarado como puntero a B, pero que swapd0 quiera que se comporte en un momento dado como si estuviese apuntando a A (como si fuese un puntero a A), siendo B un subtipo de A. El método llamado a través del puntero a B será el de la clase A, sólo cuando no este disponible y declarado en la clase B (como he explicado en el segundo párrafo). Pero si también está en la clase B, no se me ocurre como hacer referencia al método de la superclase sin meternos en los punteros raros de la STL o boost, los cuales no conozco pero lo mismo alguno lo permite.

Así que me remito a mi consejo de crearte un método aparte que resuelva por ti el comportamiento que deseas. Si te queda bonito, y cumple con lo que quieres, siempre que se pueda dar en otras ocasiones y sea una forma común de resolverlo, puedes llamarlo "patrón" (que en programación es sinónimo de "patraña") y ponerle tu nombre  :P
fjfnaranjo.com - Creating entertainment - Creando entretenimiento
fjfnaranjo [4t] gm4il [d0t] c0m (mail y msn)

Vicente

Estooo, estas seguro de que no hay diferencia entre el tipo que contenga una variable y el tipo declarado de la misma? Si estas en lo cierto C++ tendria double dispatch y no lo tiene que yo sepa.

fjfnaranjo

Um, aprovechando que responde alguien que lo ha estudiado formalmente.

Si vinculación dinámica es la traducción de Dynamic dispatch (http://en.wikipedia.org/wiki/Dynamic_dispatch), ¿como se conoce formalmente lo que yo he confundido con vinculación dinámica (http://en.wikipedia.org/wiki/Dynamic_linking) en español (v. segundo post que he puesto)?

Así lo cambio en mi post arriba y no confundo innecesariamente a alguien que lo lea.
fjfnaranjo.com - Creating entertainment - Creando entretenimiento
fjfnaranjo [4t] gm4il [d0t] c0m (mail y msn)

fjfnaranjo

Cita de: Vicente en 13 de Marzo de 2011, 07:18:40 PM
Estooo, estas seguro de que no hay diferencia entre el tipo que contenga una variable y el tipo declarado de la misma? Si estas en lo cierto C++ tendria double dispatch y no lo tiene que yo sepa.

Aquí no entiendo a que te refieres.

En C++, como en cualquier lenguaje fuertemente tipado, si tu pones "int a", a va a contener un int sí o sí.

A lo mejor no entendemos lo mismo por declarar y contener, para mi declarar es lo que el programador dice que va a tener una variable cuando la crea la primera vez (poniéndole el tipo a la izquierda) y contener es lo que se supone que representa la cadenita de 1s y 0s que va en el espacio de memoria donde sea que se asigne el contenido de a. Yo no estoy hablando de mecanismos internos del lenguaje ni nada parecido, lo que digo es que a va a contener un int y punto, ¿qué luego quieres hacer que se comporte como otra cosa?, pues ya haces casts y demás, pero en a sigue habiendo un entero, porque se supone que el lenguaje/compilador/loquesea se encarga de no dejarte hacer otra cosa.

Obviamente, como C++ es permisivo, lo de arriba no es estrictamente cierto, y en teoría puedes apuntar a "a" con un puntero de tipo int*, y usar la dirección tal cual (el valor que contiene el int*) para meter cualquier otra cosa ahí, pero eso no es que el lenguaje tenga separado el tipo contenido del declarado, eso es que tu estás accediendo a la memoria como te da la gana. C++ cuando intente hacer cualquier cosa con a, va a asumir que tiene un entero, independientemente de que tu hayas trasteado con el valor de a usando la permisividad de C++ u otro programa que tenga acceso directo a la memoria (trainers y demás).

Pero vamos, me huele que "contener" y "declarar" son palabras que estamos usando pero que no vemos igual, porque esto que estoy diciendo es algo muy básico de C y otros lenguajes similares, y los demás estáis hablando de cosas más profundas del lenguaje que yo ni siquiera conozco.
fjfnaranjo.com - Creating entertainment - Creando entretenimiento
fjfnaranjo [4t] gm4il [d0t] c0m (mail y msn)

Vicente

#12
Creo que usamos tipar y contener de la misma manera. Todos los ejemplos son de C# pero estoy casi seguro que el compilador de C++ se debe comportar de forma parecida.

Un ejemplo simple:


public class A
{
   public virtual void Foo()
   {
       Console.WriteLine("A.Foo()");
   }
}

public class B : A
{
   public override void Foo()
   {
       Console.WriteLine("B.Foo()");
   }
}

class Program
{
   static void Main(string[] args)
   {
       A a = new A();
       a.Foo();

       B b = new B();
       b.Foo();

       A ab = new B();
       ab.Foo();

       Console.ReadLine();
   }
}


Es mas o menos facil saber que saldra A.Foo(), B.Foo(), y B.Foo(). Esta claro que la tercera variable esta declarada como A pero contiene un B, son cosas diferentes.

El proceso por el cual se ha elegido ejecutar B.Foo en vez de A.Foo en la tercera linea se llama method dispatching, en particular C++, C#, Java,... tienen lo que se llama single dispatch: solo ejecutan la evaluacion dinamica del tipo sobre el metodo que se invoca el objeto.

Un ejemplo un poco mas gracioso de single dispatch.


public class A
{
   public void Foo(Arg1 arg)
   {
       Console.WriteLine("A.Foo(Arg1)");
   }

   public void Foo(Arg2 arg)
   {
       Console.WriteLine("A.Foo(Arg2)");
   }
}

public class Arg1 { }

public class Arg2 : Arg1 { }

class Program
{
   static void Main(string[] args)
   {
       Arg1 arg1 = new Arg1();
       Arg2 arg2 = new Arg2();
       Arg1 arg12 = new Arg2();
           
       A a = new A();
       a.Foo(arg1);
       a.Foo(arg2);
       a.Foo(arg12);

       Console.ReadLine();
   }
}


La salida es A.Foo(Arg1), A.Foo(Arg2), A.Foo(Arg1). Las dos primeras estan claras, pero la tercera podria dar mas dudas; si arg12 es en realidad un Arg2, porque se ha ejecutado el metodo que recibe Arg1?

Pues porque no hay double/multiple dispatch. Es decir, la firma del metodo a ejecutar (segun los argumentos) se decide en tiempo de compilacion, no en tiempo de ejecucion. Y en tiempo de compilacion lo unico que se sabe es el tipo declarado de la variable arg12 que es Arg1, asi que se elige ejecutar el metodo Arg1. El patron Visitor es simplemente un workaround para esta limitacion del lenguaje (en C# ahora lo que se puede hacer es pasar el argumento como dynamic y listo :) ).

Y luego ya por puro aburrimiento podemos poner mas a prueba la forma que tenga el compilador de resolver las sobrecargas y las sobreescrituras. Esto ya es de C#, ni idea de si el compilador de C++ se comporta igual (supongo que si, el de Java si que lo hace creo recordar)


public class A
{
   public virtual void Foo(Arg1 arg)
   {
       Console.WriteLine("A.Foo(Arg1)");
   }

   public void Foo(Arg2 arg)
   {
       Console.WriteLine("A.Foo(Arg2)");
   }
}

public class B : A
{
   public override void Foo(Arg1 arg)
   {
       Console.WriteLine("B.Foo(Arg1)");
   }
}

public class Arg1 { }

public class Arg2 : Arg1 { }

class Program
{
   static void Main(string[] args)
   {
       Arg2 arg2 = new Arg2();
           
       B b = new B();
       b.Foo(arg2);

       Console.ReadLine();
   }
}


Se ejecuta A.Foo(Arg2). Pero si hago esto otro:


public class A
{
   public void Foo(Arg2 arg)
   {
       Console.WriteLine("A.Foo(Arg2)");
   }
}

public class B : A
{
   public void Foo(Arg1 arg)
   {
       Console.WriteLine("B.Foo(Arg1)");
   }
}

public class Arg1 { }

public class Arg2 : Arg1 { }

class Program
{
   static void Main(string[] args)
   {
       Arg2 arg2 = new Arg2();
           
       B b = new B();
       b.Foo(arg2);

       Console.ReadLine();
   }
}


Se ejecuta B.Foo(Arg1).

Estaria de PM si alguien pone si C++ hace lo mismo la verdad.

fjfnaranjo

Cita de: Vicente en 15 de Marzo de 2011, 05:43:12 PM
Creo que usamos tipar y contener de la misma manera. Todos los ejemplos son de C# pero estoy casi seguro que el compilador de C++ se debe comportar de forma parecida.

Vale, ahora lo entiendo. Tu hablas de lo que hace el lenguaje cuando se encuentra con el caso de que una variable declarada como *X cuando tiene en su contenido la dirección de un objeto (digamos Y) que esta heredado de X. Aquí el lenguaje hace lo que tiene que hacer para dar soporte a POO, que es buscar el método en Y y luego en X. Esto es fácil, y asumo que de aquí partíamos todos. Cuando te dije que en C++ no había diferencia entre el método declarado y el contenido no me expliqué bien, porque si se entiende literalmente directamente no habría forma de hacer POO y el lenguaje no estaría orientado a objetos. Lo que yo decía es que el lenguaje no cambia el tipo de la variable porque esta contenga otra cosa (como hacen otros lenguajes, como JS si no me equivoco  ???).

Volviendo a la POO y al hecho de buscar el método primero en el hijo y luego en el padre...

Por los ejemplos que publicáis, se ve que lo hace así independientemente de que utilizando la sobrecarga de funciones se "fuerce" (o "aconseje", mejor) al lenguaje de hacer lo contrario (porque vosotros estáis mezclando las dos cosas, la sobrecarga de funciones y la sobreescritura [yo no sabía como llamar a esto en castellano, siempre he usado overload y override, respectivamente]).

Parece ser que sólo se fija en la parte del nombre del método, cuando lee la firma del mismo, habría que mirarlo en el estándar para saber si está previsto en el lenguaje el caso de sobrecarga de funciones y este comportamiento en particular, pero claro, el estándar de C++ tiene que ser un cristo (sólo por curiosidad, he compilado los ejemplos de C++ con --pedantic en GCC, así que sí que debe estar previsto en el estándar, o GCC nos está engañando).

Volviendo entonces al contenido original de este mensaje, si swapd0 quiere usar el método del padre, sólo puede mutilar temporalmente el objeto con la solución de Gorkinovich, u opcionalmente tratarlo como un problema en el diseño y buscarse un patrón que lo solucione o inventarse uno propio. Al respecto de esto: ¿conocéis algún patrón que ya exista que solucione este caso (sin mezclar sobrecarga)? ¿se os ocurre alguna otra forma de dar una respuesta a swapd0 que no sea la mutilación o el rediseño del problema? ¿qué pasa con la respuesta de Zaelsius ( y->A::foo(); )?
fjfnaranjo.com - Creating entertainment - Creando entretenimiento
fjfnaranjo [4t] gm4il [d0t] c0m (mail y msn)

player

Cita de: Gorkinovich en 16 de Marzo de 2011, 12:56:22 AM
Por lo que si declaramos una variable como puntero a B, mira primero en B, si no encuentra nada que sea satisfactorio, pasaría a mirar en A. Un comportamiento peculiar y peligroso si no se conoce.
En qué ejemplo de los que has puesto estaría ocurriendo esto que dices?






Stratos es un servicio gratuito, cuyos costes se cubren en parte con la publicidad.
Por favor, desactiva el bloqueador de anuncios en esta web para ayudar a que siga adelante.
Muchísimas gracias.