¿Evitar "autoinicialización" en los constructores

Dudas sobre el C/C++ en general
Responder
Mensaje
Autor
Avatar de Usuario
postit
Mensajes: 59
Registrado: 14/11/2008 9:42 am

¿Evitar "autoinicialización" en los constructores

#1 Mensaje por postit » 21/11/2009 4:27 am

Hola, me ha surgido la duda de qué pasaría ante una llamada a un constructor de copia tal que así:

Código: Seleccionar todo

entero n2(n2)
"entero" es una clase de prueba, el código de prueba completo es el siguiente:

Código: Seleccionar todo

class entero
{
public:
    entero (int num = 0) { n = num; }
    entero (const entero &e) {
        if (*this != e) // ¿haria falta?
            copia(e);
    }
    ~entero (void) { n = 0; }
    entero& operator= (const entero &e) {
        if (this != &e) {
            n = 0;
            copia(e);
        }
        return *this;
    }
    bool operator== (const entero &e) { return n == e.n; }
    bool operator!= (const entero &e) { return !(*this == e); }

private:
    int n;
    void copia (const entero &e) { n = e.n; }
};

int main (void) {
    entero n1(5);
    entero n2(n2); // ¿deberia evitarlo?
    return 0;
}
En este sencillo caso no ocurre nada si se producen autoasignaciones o autoinicializaciones (entero n2(n2), n2 = n2, ...) pero cuando hay memoria dinámica de por medio o objetos de clases todo-parte dentro de la definición de otras clases/objetos... lo veo peligroso o que debería de controlarlo.

Cada vez que me enfrento a C++ me da la sensación de que sé menos :) ¿Alguien me podría dar alguna referencia sobre este asunto o indicar si necesito añadir la comprobación "if (*this != e)" siempre que escriba constructores de copia?

Gracias por atender, un saludo.
int n[]={0x48,0x65,0x6C,0x6C,0x6F,0x2C,0x20,0x77,0x6F,0x72,0x6C,0x64,
0x21,0x0A,0x00},*m=n;main(n){putchar(*m)!='\0'?main(m++):exit(n++);}

Avatar de Usuario
daltomi
Mensajes: 355
Registrado: 28/04/2007 7:29 pm
Ubicación: Argentina

#2 Mensaje por daltomi » 21/11/2009 10:13 am

Buenas.
Se debe comprobar que no se trate del mismo objeto.
Tenemos 2 maneras de determinar igualdades en los objetos. Una por los valores que representan y la otra por la dirección a memoria en la que existen.
Cuando hacemos if(*this != e){..} estamos comparando entre objetos, o sea sus valores, por lo tanto se espera el operador != esté implementado, como existe tenemos !(*this == e) que, otra vez compara los objetos por sus valores y se espera que el operador == éste implementado, como existe lo utiliza.
La otra manera es más eficiente y es la de comparar las direcciones de memoria de los objetos, si son iguales se trata del mismo objeto: if(this != &e){..}. Más rápido.

Saludos.

Avatar de Usuario
postit
Mensajes: 59
Registrado: 14/11/2008 9:42 am

#3 Mensaje por postit » 21/11/2009 5:41 pm

Hola gracias por tan buena respuesta. Entonces debería cambiar:

Código: Seleccionar todo

if (*this != e)
            copia(e); 
Por

Código: Seleccionar todo

if (this != &e)
            copia(e); 
¿Pero debería de añadir tal comprobación en mis constructores de copia? A la hora de sobrecargar el operador de igualdad está claro que sí es necesario ¿pero y en los constructores de copia?

Saludos
int n[]={0x48,0x65,0x6C,0x6C,0x6F,0x2C,0x20,0x77,0x6F,0x72,0x6C,0x64,
0x21,0x0A,0x00},*m=n;main(n){putchar(*m)!='\0'?main(m++):exit(n++);}

Avatar de Usuario
daltomi
Mensajes: 355
Registrado: 28/04/2007 7:29 pm
Ubicación: Argentina

#4 Mensaje por daltomi » 22/11/2009 5:39 pm

Buenas.
Si, yo me referia a que en el constructor copia o en el operador= se puede comparar con (this != &e), no pasa lo mismo con el operador!= donde si uno quiere comparar valores entre objetos.
Entonces añadir la comprobación en el ejemplo que muestras estaria bien, asi evitas que el cliente de tu clase pueda usar un constructor copia sobre el mismo objeto.
Ten presente cuando implementas un constructor copia que utiliza listas de inicialización.
Ejemplo escrito al vuelo.

Código: Seleccionar todo

class A 
{
        public:
        A() : n(0) { };
        A(const int i) n(i) { ++n; };
        A(const A& a) : n(a.n)
        {
          if(this != &a){ ++n; }
        };

        private:
        int n;
};
Para cuando se ejecuta this != &a ya es muy tarde. Peor todavía en una derivada.

Unas correciones:

Código: Seleccionar todo

~entero (void) { n = 0; } 
No es necesario void y no es necesario "resetear" n (que no es estático) ya que cuando el objeto se destruye n deja de existir. Lo mismo en el operador de asignación, asignar a n=0 y despues inmediatamente n=e.n;

Saludos.

Avatar de Usuario
postit
Mensajes: 59
Registrado: 14/11/2008 9:42 am

#5 Mensaje por postit » 23/11/2009 8:57 am

Hola de nuevo,

Entonces añadiré la comprobación "if(this != &...)" en el cuerpo de mis constructores de copia a partir de ahora (al igual que llevo haciendo en la sobrecarga de la asignación).

Sobre el uso de la lista de incializadores de un constructor, tu ejemplo deja claro que aunque los constructores de copia tienen tales listas de inicializadores no deberían de usarse ¿no? Ya que en tu ejemplo

Código: Seleccionar todo

A(const A& a) : n(a.n)
        {
          if(this != &a){ ++n; }
        };
El inicializador "n(a.n)" se ejecuta primero que el propio codigo/bloque del constructor no? Parece que para constructores de copia no se debería de utilizar lista de inicializadores.

Sobre el uso de Listas de inicializadores de los constructores, nunca he tenido claro cuando debería utilizarlas, he leído acerca de ellas pero no me queda claro en qué ocasiones debería utilizar listas de inicializadores y en qué ocasiones debería hacer tales inicializaciones dentro del cuerpo del constructor.

Saludos
int n[]={0x48,0x65,0x6C,0x6C,0x6F,0x2C,0x20,0x77,0x6F,0x72,0x6C,0x64,
0x21,0x0A,0x00},*m=n;main(n){putchar(*m)!='\0'?main(m++):exit(n++);}

Avatar de Usuario
daltomi
Mensajes: 355
Registrado: 28/04/2007 7:29 pm
Ubicación: Argentina

#6 Mensaje por daltomi » 23/11/2009 9:18 pm

Buenas.
Voy a tratar de explicarte lo que he entendido sobre estos temas.

Ejemplo de asignación sin lista de inicialización con un miembro string str(no es constante y no es una referencia)

Código: Seleccionar todo

class A 
{
    public:
        A(const string& s) {str = s; };
        A(const A& a) { str=a.str; };
    private:
        string str;
};
Cuando A se construye tambien lo hacen sus miembros, asi string str siempre llama a su ctor. antes de entrar al cuerpo del ctor. de A. ¿Cual de todos sus contructores string llama? En el ejemplo de asignación llama a su ctor. por defecto y luego, como en el cuerpo del ctor. de A se utiliza la asignación, se llama al operador= de str, por lo tanto son dos llamadas para string str.
Gracias a la lista de inicialización se puede evitar ésto, cuando str de construye se llama a su ctor. copia y listo: str(a) o str(a.str)
Es muy importante entender que en una lista de inicialización los parámetros no son iniciados según el orden que uno les de, sino como estan declarados dentro de la clase. No importa hacer A() : a(0),b(0), si en la clase estan declarados: int b; int a. Inicia b primero.

Existen casos en que es obligatorio llamar a un ctor.(ya sea por defecto o copia), con lista de inicialización, y esto de debe a dos cosas.
Un tipo const y una referencia deben ser siempre inicializados, nunca asignados.
Un ejemplo que obliga a usar lista de inicialización:
Primero el main que no cambia para los ejemplos siguientes.

Código: Seleccionar todo

int main()
{
    const string mensaje("Hola");
    A a1(mensaje);
    A a2(a1);
}
Error por asignación y además ineficiente.

Código: Seleccionar todo

class A 
{
    public:
        A(const string& s) {str = s; };
        A(const A& a) { str = a.str; };
    private:
        const string str;
        
        //o tambien...
        //string& str;
        //const string& str;
};
Solución con lista de inicialización.

Código: Seleccionar todo

class A 
{
    public:
        A(const string& s) : str(s) { };
        A(const A& a) : str(a.str)  { };
    private:
        const string str;
        
        //o tambien...
        //string& str;
        //const string& str;
};
Para la herencia con ctor. copia o por defecto la lista de inicialización se utiliza tambien:

Código: Seleccionar todo

class B
{
    public:
        .........
        B(const B& b) : sb(b.sb) { };
    private:
        string sb;
};

class A : public B
{
    public:
        ........
        A(const A& a) : B(a), str(a.str) { };
        .........
    private:
        const string str; 
};
Dónde entiendo(espero traducir built-in type) que no existiría mucha diferencia en utilizar lista de inicialización o asignación es por ejemplo:

Código: Seleccionar todo

class A
{       ...
        A() : a(0), b(0), c(0), d(1), e(1), f(1) { };
        ...
        int a, b, c;
        double d, e, f;
};
Alto margen de cometer un error y dificil de mantener.
Solución:

Código: Seleccionar todo

class A
{       ...
        A() { init(); };
        ...
        int a, b, c;
        double d, e, f;
        
        init()
        {
          a = b = c = 0;
          d = e = f = 1;
        }
};
Saludos.

Avatar de Usuario
postit
Mensajes: 59
Registrado: 14/11/2008 9:42 am

#7 Mensaje por postit » 24/11/2009 4:24 pm

Hola de nuevo daltomi, he leído tus explicaciones (bastante claras) pero todavía no he tenido tiempo para analizarlas con algo de calma e ir haciendo alguna prueba pero lo haré descuida. Solo era para mandarte un saludo y agradecerte el esfuerzo :)

Saludos
int n[]={0x48,0x65,0x6C,0x6C,0x6F,0x2C,0x20,0x77,0x6F,0x72,0x6C,0x64,
0x21,0x0A,0x00},*m=n;main(n){putchar(*m)!='\0'?main(m++):exit(n++);}

Avatar de Usuario
cheroky
Mensajes: 2571
Registrado: 22/09/2005 11:00 pm
Ubicación: Valladolid (España)

#8 Mensaje por cheroky » 16/12/2009 7:07 pm

Una forma rudimentaria y sencilla de identificar una instancia de clase es utilizar un id seteado sobre un contador static, y otra mas robusta es almacenar ese mismo id en una lista también static, dicha lista invalida cualquier identificador no almacenado. Si se utilizan hilos o se comparten procesos se gestiona mediante semáforos.

Por otra parte y opcionalmente, la implementación del binomio constructor copia y operador de asignación cuando son coincidentes en el diseño y no hay una "preconstrución" de la instancia pueden complementarse del siguiente modo.

Operador de asignación.

Código: Seleccionar todo

    const T& operator=(const T& e)
    {
        // ---
        return *this;
    }
Constructor copia.

Código: Seleccionar todo

    T(const T& e)
    {
        *this = e;
    }
Otras sobrecargas del operador de asignación son legales.

*EOF*
Imagen

Responder

¿Quién está conectado?

Usuarios navegando por este Foro: Derikjeow, JerodBige y 8 invitados