martes, 27 de septiembre de 2011

Funciones, code forwarding y slicing

Está claro que cuando declaramos una función estamos estableciendo como nos comunicaremos con el código cliente. Pero todos conocemos que en c++ existen 3 maneras diferentes de pasar parámetros: por valor, por parámetros y por referencia. Como deberíamos diseñar pues la interface de nuestras funciones? Segun la guia de estilo de google deberíamos diseñar las funciones poniendo las variables de entrada y salida como punteros y las variables de entrada como referencias constantes. Algo asi:


type1& funct(const type2& ref, type3* point){}



Salvando memoria
La primera ventaja de las referencias en las funciones y métodos va por triplicado. Por un lado es transparente para el usuario, puesto que podemos insertar las variables sin semántica de punteros y manejarlas dentro de la función de la misma forma. Además, consumimos menos memoria, puesto que solo copiamos la referencia del parametro y no copiamos el objeto. Para darnos cuenta de cuando copiamos un objeto, una manera muy buenas es usar una macro como esta:


// A macro to disallow the copy constructor and operator= functions
// This should be used in the private: declarations for a class
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
  TypeName(const TypeName&);               \
  void operator=(const TypeName&)




Después podemos declarar nuestras clases asi:


class Foo {
 public:
  Foo(int f);
  ~Foo();

 private:
  DISALLOW_COPY_AND_ASSIGN(Foo);
};




Gracias a esto, nos aseguramos que el compilador nos prohiba copiar un objeto y pare la compilación. Es una buena costumbre la de usar el compilador para protegernos de errores.

Code forwarding
La segunda ventaja viene al diseñar clases , con el code forwarding. Cuando la interface de una clase solo declara punteros a otra clase, no necesitamos la cabecera para definirlos. Esto es: si tenemos una clase asi


class Forwarded;

class Declared{

    Forwarded* m_for;

    void fund(Forwarded* f);

};



Esta clase compilaria perfectamente, puesto que Forwarded son punteros asi que el compilador no necesita conocer ningun detalle más de Forwarded para compilar. Lo unico que necesitamos es declarar la clase de manera adelantada. Es una manera de decirle al compilador: "existe una clase llamada Forwarded. Tu compila, que luego de doy mas detalles". Pero cuando le damos los detalles? Pues en el fichero de codigo, en el .cpp, es donde se hace el include.

Y que ganamos con esto? La mayoria del tiempo que tarda la compilación es en operaciones recursivas de inclusion de codigo, o sea, en parseo. Cada vez que variamos una parte del codigo de un fichero afecta a todas las cabeceras incluidas de manera recursiva en esa unidad de compilacion. Declarando el include en el fichero .cpp retiramos esta cabecera del circuito y hacemos que la compilación sea mucho mas rápida

Slicing (rebanamiento)
La tercera ventaja es que evitamos el slicing. El slicing es un problema de c++ al copiar en una variable de tipo A, una variable de tipo B que deriva A. Si tenemos las clases


class A{

public:

    int t1;

};

class B: public A{

public:

    int t2;

};


Si hacemos esto:


B b;

b.t1 = 2;

b.t2 = 3;

A a = b;

// t2 no ha sido copiado.

B b2 = a;

assert(b2.t2 == b.t2); // esto fallará


Que ha pasado? Pues que en la copia entre clases, A no tiene un miembro t2, asi que no lo copia. Cuando volvemos a copiar A en B, ese miembro ya no esta en A y por tanto es indefinido en B. Este caso es claro en el codigo anterior pero se puede hacer más dificil de ver en el paso de parametros en una funcion que acepte parametros polimorficos. Al pasar la referencia, ayudamos a prevenir esta perdida de información. Por ejemplo


B* b = new B;

b->t1 = 2;

b->t2 = 3;

A* a = b;

B* b2 = a;

assert(b2->t2 == b->t2); //OK

No hay comentarios:

Publicar un comentario