C++ FAQ

Constructores

¿Qué hacen los constructores?

Los constructores construyen objetos del polvo.

Los constructores son como “funciones de inicio”. Ellos a su vez convierten un montón de bits arbitrarios en un objeto vivo. Mínimamente inicializa las variables miembro de la clase. También puede asignar recursos (memoria, archivos, semáforos, sockets, etc.)

“ctor” es una abreviatura típica para referirse al constructor.

¿Hay alguna diferencia entre Lista x, y Lista x();?

Hay una gran diferencia!

Supongamos que Lista es el nombre de alguna clase. Entonces la función f() declara un objeto local de la clase Lista llamado x:

void f()
{
   List x;     // Objeto local llamado x (de la clase Lista)
   ...
}

Pero la función g() declara una función llamada x() que devuelve una lista:

void g()
{
   List x();   // Funcion x (que retorna una Lista)
   ...
}

¿Puede un constructor llamar a otro en la misma clase para inicializarlo?

No!

Vamos a ver un ejemplo. Supongamos que usted desea que su constructor Algo::Algo(char) llama a otro constructor de la misma clase, por ejemplo Algo::Algo(char, int), a fin de que Algo::Algo (char, int) ayudaría a inicializar el objeto de este . Desafortunadamente no hay manera de hacer esto en C++.

Algunas personas lo hacen de todos modos. Por desgracia, no logran lo que quieren. Por ejemplo, la línea Algo(x, 0); no llama Algo::Algo(char, int) en el objeto this. En cambio, la llamada Foo::Foo(char, int) inicializa un objeto temporal local (no this), éste inmediatamente se destruye despues del punto y coma (;).

class Algo {
public:
  Algo(char x);
  Algo(char x, int y);
  ...
};
 
Algo::Algo(char x)
{
  ...
  Algo(x, 0);  //esta linea  NO ayuda a inicializar el objeto this!!
  ...
}

Algunas veces se puede combinar dos constructores a través de un parámetro por defecto:

class Algo {
public:
  Algo(char x, int y=0);  // esta linea simula dos contructores
  ...
};

Si eso no funciona, por ejemplo, si no hay un parámetro predeterminado adecuado que combine los dos constructores, usted puede crear una funcion privada init() que inicialice todo lo que necesite, la cual deberá llamarla desde todos los constructores:

 class Algo {
 public:
   Algo(char x);
   Algo(char x, int y);
   ...
 private:
   void init(char x, int y);
 };
 
 Algo::Algo(char x)
 {
   init(x, int(x) + 7);
   ...
 }
 
 Algo::Algo(char x, int y)
 {
   init(x, y);
   ...
 }
 
 void Algo::init(char x, int y)
 {
   ...
 }

No intente solucionarlo reemplazando el objeto this. Algos piensan que pueden escribir new(this) Algo(x, int(x)+7) en el cuerpo del constructor Algo::Algo(char). Sin embargo esto es malo, muy malo. Por favor no me escriba diciendo que eso puede hacerse en su version particular de compilador; eso se ve mal. Los constructores hacen un montón de pequeñas cosas mágicas detrás del telón, pero a las malas técnicas sobre bits parcialmente construidos sólo diga no.

¿Es Freed:Freed() el constructor por defecto de Freed?

No.

Un “constructor por defecto” es un constructor que puede ser llamado sin argumentos. Un ejemplo de esto es un constructor sin ningún parámetro:

class Fred {
 public:
   Fred();   // Constructor por defecto, puede ser llamado sin argumentos
   ...
};

Otro ejemplo de un “constructor por defecto” es uno que puede tener argumentos, siempre y cuando tenga valores por defecto:

class Fred {
 public:
   Fred(int i=3, int j=5);   // Default constructor: can be called with no args
   ...
};

¿Qué constructor es llamado al crear un array de objetos Freed?

El constructor por defecto es llamado cuando se crea un array de objetos (excepto como se describe más adelante).

class Fred {
 public:
   Fred();
   ...
 };
 
 int main()
 {
   Fred a[10];              //← llama al constructor por defecto 10 veces
   Fred* p = new Fred[10];  //← llama al constructor por defecto 10 veces
   ...
}

Si la clase no tiene un constructor por defecto, obtendrá un error en tiempo de compilación cuando intente crear un array con la sintaxis sencilla de arriba:

class Fred {
 public:
   Fred(int i, int j);     // ← se asume que no es el constructor por defecto
   ...
 };
 
 int main()
 {
   Fred a[10];             // ← ERROR: Fred no tiene un constructor por defecto
   Fred* p = new Fred[10]; // ← ERROR: Fred no tiene un constructor por defecto
   ...
}

Sin embargo, incluso si la clase ya tiene un constructor por defecto, usted debe tratar de usar std::vector en lugar de un array (usar arrays son mala idea). std::vector le permite decidir usar cualquier constructor, no sólo el constructor por defecto:

#include 
int main()
{
   std::vector a(10, Fred(5,7));  //← los 10 objetos Fred en std::vector
                                  //      son inicializados con Fred(5,7)   
   ...
}

A pesar de que usted puede utilizar un std::vector en lugar de una matriz, hay veces en que un array puede ser lo correcto, y para esos casos, es posible que tenga que usar la sintaxis de “inicialización explícita de las arrays”. He aquí cómo:

class Fred {
 public:
   Fred(int i, int j);     // ← assume there is no default constructor
   ...
};
 
int main()
{
   Fred a[10] = {
     Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7),  // Los 10 objetos Fred son
     Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7)   // initializados usando Fred(5,7)
   };
   ...
}

Por supuesto usted no tiene que poner Fred (5,7) para cada entrada – usted puede poner cualquier número que desee, incluso parámetros u otras variables.

Por último, puede utilizar el reemplazo del constructor para inicializar manualmente los elementos de la matriz. Advertencia: se vería muy mal: la matriz en bruto no puede ser de tipo Fred, por lo que tendrás que implementar macros para calcular los punteros como operaciones de índice. Advertencia: depende del compilador y del hardware: usted tendrá que asegurarse de que el almacenamiento sea tan estricto para asegurarse que esta alineado como para guardar los objetos de la clase Fred. Advertencia: es tedioso para que este libre de una excepción: tendrás que destruir manualmente los elementos, incluso en el caso cuando se produce una excepción parcial a través de un bucle, llama a los constructores. Pero si realmente quieres hacerlo de todos modos, leer todo acerca de reemplazo del constructor. (Por cierto el reemplazo del constructor es la magia que se utiliza dentro de std::vector. La complejidad que hace que todo funcione bien es otra razon para usar std::vector)

Hay que completar las referencias de placement new de parashift.com/c++-faq-lite/dtors.html#faq-11.10

¿Debe mi constructor utilizar “listas de inicialización” o “asignación”?

Es recomendable usar las listas de Inicialización. De hecho, los constructores deben inicializar por lo general todos los objetos miembro en la lista. Una excepción se discute más abajo.

Considere el siguiente constructor que inicializa la variable miembro x_ usando una lista de inicialización: Fred::Fred(): x_ (lo_que_sea) { } El beneficio de hacerlo es mejorar el rendimiento. Por ejemplo, si la expresión lo_que_sea es del mismo tipo que la variable miembro x_, el resultado de la expresión es asignado directamente a x_ -el compilador no hace una copia separada del objeto. Incluso si los tipos no son los mismos, el compilador suele ser capaz de hacer un mejor trabajo con listas de inicialización que con las asignaciones.

La otra manera (ineficiente) de declarar constructores es a través de asignación, por ejemplo: Fred::Fred () {x_ = lo_que_sea;} En este caso la expresión lo_que_sea hace que se cree un objeto temporal, y este objeto temporal es pasado al objeto x_ mediante el operador de asignación. Luego, ese objeto temporal es destruido despues del punto y coma (;). Eso es ineficiente.

Como si eso no fuera suficiente, hay otra fuente de ineficiencia cuando se utiliza asignación en un constructor: el objeto miembro estará completamente “construido” por su constructor por defecto, y esto podría, por ejemplo, asignar una cierta cantidad predeterminada de memoria o abrir algún archivo por defecto. Todo este trabajo podría ser en vano si el cualquier expresión y/o operador de asignación hace que el objeto cierre el archivo y/o libere la memoria (por ejemplo, si el constructor por defecto no asignó suficientemente memoria o si abre el archivo incorrecto).

Conclusión: Todas las cosas en igualdad de condiciones, el código se ejecutará más rápido si se utilizan listas de inicialización en lugar de asignación.

[color=#0000BF]Nota[/color]: No hay diferencia de rendimiento si el tipo de dato x_ es cierto tipo de dato incorporado o tipo intrínseco de dato, como int, char * o float. Pero incluso en estos casos, prefiero establecer los datos miembro en la lista de inicialización en lugar de la asignación, para que sea consistente. Otro argumento similar en favor del uso de las listas de inicialización, incluso para los tipos de datos incorporados/intrínsecos: const no estáticos y referencias no estáticas de variables miembro no se pueden asignarles un valor en el constructor, por lo que la simetría tiene sentido para inicializar todo en la lista de inicialización.

Ahora para las excepciones. Todas las reglas tienen excepciones, hay un par de excepciones a las reglas de “listas de inicialización”. En el fondo es usar el sentido común: si es menos costoso para el procesador, mejor, más rápido, etc., mejor no usarlos, por todos los medios, no los use. Esto puede ocurrir cuando la clase tiene dos constructores que necesitan inicializar los datos miembro del objeto this en diferentes etapas. O puede ocurrir cuando dos miembros de datos son auto-referenciados. O cuando un dato miembro necesita una referencia al objeto this, y quiere evitar una advertencia del compilador sobre el uso de la palabra clave this antes de la llave ({) con el que comienza el cuerpo del constructor (cuando su compilador, emite esta advertencia en particular). O cuando lo que necesita es hacer un if/throw en una variable (parámetro global, etc) antes de usar esa variable para inicializar uno de sus miembros de this. Esta lista no es exhaustiva, por favor no me escriba pidiéndome que añada otro “O cuando …”. El punto es simplemente este: usar el sentido común.

hay que poner referencias an if/throw test on a variable parashift.com/c++-faq-lite/ctors.html#faq-10.6

¿Debería usar el puntero this en el constructor?

Algunas personas sienten que no deben usar el puntero this en un constructor, porque el objeto no está completamente formado. Sin embargo, usted puede utilizar this en el constructor (en el {cuerpo}, e incluso en la lista de inicialización) si se tiene cuidado.

Algo que [color=#000040]siempre funciona[/color]: el {cuerpo} de un constructor (o una función llamada desde el constructor) de forma fiable puede tener acceso a los miembros de datos declarados en una clase base y/o los miembros de datos declarados en la propia clase del constructor. Esto se debe a que se garantiza que todos los miembros de datos han sido construidos/inicializados completamente en el momento que el {cuerpo} del constructor comienza a ejecutarse.

Algo que [color=#000040]nunca funciona[/color]: el {cuerpo} de un constructor (o una función es llamada desde el constructor) no puede “bajar” a una clase derivada de llamar a una función miembro virtual que reemplaza en la clase derivada. Si su objetivo era llegar a la función que reemplaza en la clase derivada, no conseguirá lo que quiere. Tenga en cuenta que no llegará a la función de reemplazo (virtual) de la clase derivada independiente de cómo usted llame a la función virtual miembro: de forma explícita con el puntero this (por ejemplo, this->metodo()), de manera implícita con el puntero this (por ejemplo, método() ), o incluso llamar a alguna otra función que llama a la función miembro virtual con el objeto this. En resumen: incluso si alguien esta construyendo un objeto de una clase derivada, durante el proceso de construccion de la clase base, [color=#000080]su objeto no es aún de esa clase derivada[/color]. Usted ha sido advertido.

Algo que [color=#000040]a veces funciona[/color]: si usted pasa cualquier dato miembro del objeto this a otro miembro en la inicialización, debe asegurarse de que los miembros de los otros datos ya se ha inicializado. La buena noticia es que usted puede determinar si el dato miembro se ha (o no) inicializado con algunas reglas del lenguaje que son independientes del compilador en particular que esté usando. La mala noticia es que usted tiene que saber las reglas del lenguaje (por ejemplo, saber si los objetos de la clase base de una sub-clase se inicializan primero, también si le permite la herencia y/o funciones virtuales múltiples, entonces los miembros de datos definidos en la clase se inician en el orden en que aparecen en la declaración de la clase). Si usted no conoce estas reglas, entonces no pase ningún miembro de datos del objeto this (independientemente de si está o no de forma explícita el uso de la palabra clave this) al inicializador de cualquier otro dato miembro! Y si usted sabe las reglas, por favor tenga mucho cuidado.

vincular parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5 y parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.5

¿Cuál es la técnica conocida como “named constructor”?

Es una técnica que proporciona operaciones de construcción de objetos más intuitivas y/o seguras para los usuarios de su clase.

El problema es que los constructores siempre tienen el mismo nombre que la clase. Por lo tanto la única manera de diferenciar entre los distintos constructores de una clase es por la lista de parámetros. Cuando hay un montón de constructores, las diferencias entre ellos convertido en algo sutil y propenso a errores.

Con “named constructors”, usted puede declarar todos los constructores de la clase en las sección privadas o protegida, y proporcionar métodos estáticos públicos que devuelven un objeto. Estos métodos estáticos son los llamados “named constructors” En general, existe uno de estos métodos estáticos para cada forma diferente de construir un objeto.

Por ejemplo, supongamos que estamos construyendo una clase Point que representa una posición en el plano XY. Resulta que hay dos formas comunes para especificar una coordenada 2D: coordenadas rectangulares (X + Y), las coordenadas polares (Radio + Angulo). (No se preocupe si usted no puede recordar estos, el punto no es los detalles de los sistemas de coordenadas;. El punto es que hay varias maneras de crear un objeto Point) Desafortunadamente los parámetros de estos dos sistemas de coordenadas son las mismas : dos tipos float. Esto crearía un error de ambigüedad en los constructores sobrecargados:

 class Point {
 public:
   Point(float x, float y);     // Coordenadas rectangulares
   Point(float r, float a);     // Coordenadas polares (radio y angulo)
   // ERROR: La sobrecarga de constructores ambiguo Point::Point(float,float)
 };
 
 int main()
 {
   Point p = Point(5.7, 1.2);   // Ambiguo: Cual sistema de coordinadas sera?
   ...
 }

Una manera de resolver la ambiguedad es usando los “named constructors”:

#include                // Para std::sin() and std::cos()
 
 class Point {
 public:
   static Point rectangular(float x, float y);      // Coordenadas Rectangulares
   static Point polar(float radius, float angle);   // Coordenadas polares
   // Estos metodos estaticos tambien son llamados "named constructors"
   ...
 private:
   Point(float x, float y);     // Coordenadas rectangulares
   float x_, y_;
 };
 
 inline Point::Point(float x, float y)
   : x_(x), y_(y) { }
 
 inline Point Point::rectangular(float x, float y)
 { 
    return Point(x, y); 
 }
 
 inline Point Point::polar(float radius, float angle)
 { 
    return Point(radius*std::cos(angle), radius*std::sin(angle)); 
 }

Ahora los usuarios de la clase tienen claro el asunto de crear una nueva instancia de la clase Point en ambos sistemas de coordenadas:

 int main()
 {
   Point p1 = Point::rectangular(5.7, 1.2);   // Obviamente rectangular
   Point p2 = Point::polar(5.7, 1.2);         // Obviamente polar
   ...
 }

Asegurese que sus constructores estén en la sección protejida si usted espera tener clases derivadas de la clase Point.

Los “named constructors” pueden ser usados para asegurar que sus objetos sean creados de una forma específica con new.

Note que los “named constructors”, al menos como se ha mostrado arriba, es solo una forma rápida de inicializar una clase, los compiladores modernos no crearán una copia extra de sus objetos creados.

¿Significa que return-by-value creará copias extras de los datos?

No necesariamente.

Todos(?) los compiladores comerciales optimizan las copias extra, al menos en casos como se ilustra en el anterior FAQ.

Para mantener el ejemplo claro, vamos a ver la esencia de las cosas. Supongamos que la función caller() llama a rbv() (“rbv” significa “return-by-value” o retorno de valor), que devuelve un valor del tipo Foo:

 class Foo { ... };
 
 Foo rbv();
 
 void caller()
 {
   Foo x = rbv(); ← el valor retornado por rbv() se asigna a x
   ...
 }

Ahora la pregunta es, ¿Cuántos objetos de Foo se habrán creado? ¿rbv() creará un objeto temporal del tipo Foo que se copiará en x? ¿cuántos temporales se han creado? Dicho de otra forma, return-by-value necesariamente afecta el rendimiento?

El punto en este tema es que la respuesta es No, los compiladores C++ de grado comercial implementan retorno por valor (return-by-value) de un modo que les permite eliminar la sobrecarga, por lo menos en los casos más sencillos como los mostrados en el anterior FAQ. En particular, todos los compiladores C++ de grado comercial optimizar este caso:

 Foo rbv()
 {
   ...
   return Foo(42, 73); ← supóngase que Foo tiene un constructor Foo::Foo(int a, int b)
 }

Ciertamente el compilador puede permitirse crear un objeto temporal local Foo, entonces copia la construcción temporal en la variable x mediaente caller(), entonces destruye el objeto temporal. Sin embargo, no todos los compiladores C++ de grado comercial lo harán: La instrucción return construirá directamente el objeto x en sí mismo. No es una copia de x, no es un puntero a x, no es una referencia a x, pero sí es x en si mismo.

Usted puede parar aquí si no quiere entender realmente el párrafo anterior, pero si quieres saber el ingrediente secreto (por lo que puede, por ejemplo, prever de manera fiable cuando el compilador puede y no puede disponer que la optimización para usted), el clave está en saber que los compiladores suelen implementar return-by-value a través de punteros. Cuando la función caller() llama a rbv(), el compilador secretamente pasa un puntero a la ubicación de rbv() que supuestamente tiene al crearse el objeto. Podría ser algo como esto (se muestra como un void *a en vez de Foo*, ya que el objeto aún no se ha construido):

// Pseudo-codigo
 void rbv(void* put_result_here) ← Codigo C++ original: Foo rbv()
 {
   ...    ← Nota: rbv() inicializa (no asigna) la variable apuntada por *put_result_here
 }
 
 // Pseudo-codigo
 void caller()
 {
   //Codigo original C++: Foo x = rbv()
   struct Foo x; ← Nota: x no es inicializado antes de llamar a rbv()
   rbv(&x);      ← Nota: rbv() initializa la variable loval definida in caller()
   ...
 }

Así que el primer ingrediente en la receta secreta es que el compilador (generalmente) transforma el return-by-value en un pass-by-pointer o paso de variable por puntero. Esto significa que los compiladores de calidad comercial no se molestan en crear un temporal: ellos crean de manera directa la construcción del objeto devuelto en la ubicación indicada por put_result_here.

El segundo ingrediente en la receta secreta es que los compiladores suelen implementar constructores usando una técnica similar. Este es un segmento de código que depende del compilador y un tanto idealizado (estoy ignorando deliberadamente la forma de manejar new y sobrecarga), pero los compiladores suelen implementar Foo:: Foo (int a, int b) usando algo como esto:

 // Pseudo-codigo
 void Foo_ctor(Foo* this, int a, int b) ← Código Original C++: Foo::Foo(int a, int b)
 {
   ...
 } 

Poniendo todo esto junto, el compilador puede aplicar la sentencia return en rvb() simplemente pasando put_result_here como el constructor del puntero this:

 // Pseudo-codigo
 void rbv(void* put_result_here) ← Codigo C++ original: Foo rbv()
 {
   ...
   Foo_ctor((Foo*)put_result_here, 42, 73); ← Codigo C++ original: return Foo(42,73);
   return;
 } 

Entonces caller() pasa la referenca de x: &x hacia rbv() luego pasa &x al constructor (como puntero this). Esto significa que el constructor construye directamente x.

En los años 90 hice un seminario para el grupo de compilador de IBM en Toronto, y uno de sus ingenieros me dijeron que se encontraron con esta optimización de return-by-value que es tan rápido, aunque no tenga habilitada la optimización en el compilador. Debido a que la optimización del retun-by-value hace que el compilador genere menos código, sino que incluso mejora los tiempos de compilación, además de hacer que su código generado más pequeño y más rápido. El punto es que la optimización de return-by-value es casi universalmente implementado, al menos en los casos mostrados anteriormente.

[color=#000040]Nota final[/color]: esta discusión se limita a que si habrá copias adicionales del objeto devuelto en una llamada de return-by-value. No hay que confundir eso con otras cosas que podrían suceder en caller(). Por ejemplo, si cambia el código de la función caller(), Foo x = rbv(), de esta forma: Foo x; x = rbv(); (note, después de la declaración), el compilador necesitará utilizar el operador de asignación, a menos que el compilador pueda demostrar que el constructor de Foo por defecto seguido del operador de asignación es exactamente el mismo que el constructor de copia, el compilador es requerido por el lenguaje para poner el objeto devuelto en un temporal en la función caller(), utiliza el operador de asignación para copiar el temporal en x, y luego destruirá el temporal creado. La optimización de return-by-value sigue jugando su papel ya que sólo será temporal, pero al cambiar de Foo x = rvb(), a Foo x; x = rbv();, ha impedido que el compilador elimine el ultimo temporal.

¿Qué hay acerca de devolver el valor de una variable local? ¿La variable local existe como un objecto separado, o es optimizado durante la ejecución?

Cuando el código devuelve una el valor de una variable local (return-by-value), el compilador puede optimizar las variables locales completamente -cero costo-espacio y cero costo-tiempo- la variable local no existe en realidad como un objeto distinto del que se espera conseguir (ver más abajo para obtener información específica acerca de lo que esto significa). Otros compiladores no optimizan esto de esa forma.

Estos son algunos de los compiladores que optimizan las variables locales completamente (!):

  • GNU C++ (g++) desde la versión 3.3.3
  • (Hay otros pero necesito mas información)

Estos son algunos de los compiladores que no optimizan las variables locales (!):

  • Microsoft Visual C++.NET 2003
  • (Hay otros pero necesito mas información)

Aquí está un ejemplo que muestra lo que queremos decir en esta pregunta:

 class Foo {
 public:
   Foo(int a, int b);
   void algun_metodo();
   ...
 };
 
 void hacer_algo_con(Foo& z);
 
 Foo rbv()
 {
   Foo f = Foo(42, 73);
   f.algun_metodo();
   hacer_algo_con(y);
   return f;
 }
 
 void caller()
 {
   Foo x = rbv();
   ...
 }

La cuestión que se aborda en esta FAQ es la siguiente: ¿Cuántos objetos Foo, en realidad se crean en tiempo de ejecución? Conceptualmente puede haber un máximo de tres objetos distintos: el temporal creado por Foo(42, 73), la variable f (en rbv()), y la variable x (en caller()). Sin embargo, como hemos visto anteriormente la mayoría de los compiladores fusionan Foo(42, 73) y la variable f en el mismo objeto, lo que reduce el número total de objetos de 3 a 2. Sin embargo, esta FAQ lleva un paso más allá: ¿la variable f (en rbv()) se muestra como un objeto distinto de x (en caller()) en tiempo de ejecución?

Algunos compiladores, incluyendo pero no limitado a los anteriores, optimizan completamente variable local f. En estos compiladores, no es sólo un objeto de Foo en el código anterior: la variable x en caller() es exactamente la misma variable f en rbv().

Lo hacen de la misma manera como se describió anteriormente: el retorno por valor de la función rbv() se implementa como un paso de puntero (pass-by-pointer), donde el puntero apunta a la ubicación en el objeto devuelto a ser inicializado.

Así que en lugar de construir f como un objeto local, estos compiladores simplemente contruyen *put_result_here, y todo el tiempo ven la variable f usado en el código fuente original que es sustituida por *put_result_here. Luego en la línea return y; se convierte simplemente en return; ya que el objeto devuelto ya se ha construido en el lugar designado por la funcion caller().

Veamos el (pseudo) codigo de resultado:

 // Pseudo-codigo
 void rbv(void* put_result_here)             ← Código C++ original: Foo rbv()
 {
   Foo_ctor((Foo*)put_result_here, 42, 73);  ← Código C++ original: Foo f = Foo(42,73);
   Foo_algun_metodo(*(Foo*)put_result_here);  ← Código C++ original: f.algun_metodo();
   hacer_algo_con((Foo*)put_result_here); ← Código C++ original: hacer_algo_con(f);
   return;                                   ← Código C++ original: return f;
 }
 
 void caller()
 {
   struct Foo x;                             ← Nota: x no es inicializado aun.
   rbv(&x);                                  ← Código C++ original: Foo x = rbv();
   ...
 }

Advertencia: esta optimización aplica sólo cuando todas sentencias return devuelven la misma variable local. Si una sentencia return en rbv() devuelve la variable local f, pero otra sentencia return (en otra linea de código) retorna otra cosa como una variable temporal o global, el compilador no va a poder crear la variable en el lugar donde caller() le diga: x. Verificar que las declaraciones de todas las sentencias return devuelvan la misma variable local requiere un trabajo extra por parte de los que hacen los compiladores, lo que es la razon por la que normalmente algunos compiladores fallan en la optimización del retorno de valores locales.

Nota final: esta discusión se limita a si habrá copias adicionales del objeto devuelto en una llamada de return-by-value. No hay que confundir eso con otras cosas que podrían suceder en caller(). Por ejemplo, si cambia el código de caller() de Foo x = rbv(); a Foo x; x = rbv(); (note el ; después de la declaración), el compilador va a utilizar el operador de asignación para Foo, no podemos asegurar que que el constructor por defecto de Foo seguido por el operador de asignación es exactamente el mismo que la copia del constructor, el compilador es requerido por el lenguaje para devolver el objeto en un anónimo temporal en caller(), utiliza el operador de asignación para copiar el temporal en x, entonces destruye el temporal. La optimización de return-by-value sigue jugando su parte ya que sólo será temporal, pero al cambiar de Foo x = rbv(), a Foo x; x = rbv();, han impedido que el compilador elimine el ultimo temporal creado.

¿Por qué no puedo inicializar un miembro estático en los constructores?

Debido a que debe definir explícitamente los datos miembro estáticos de la clase.

Fred.h:

 class Fred {
 public:
   Fred();
   ...
 private:
   int i_;
   static int j_;
 };

Fred.cpp (or Fred.c o lo que sea):

 Fred::Fred()
   : i_(10)  // OK: usted puede (y debe) inicializar el dato miembro de esta forma
   , j_(42)  // Error: usted no puede inicializar los datos miembros estáticos de esta forma
 {
   ...
 }
 
 // Ustede debe definir los datos miembro de esta forma:
 int Fred::j_ = 42;

Nota: in algunos casos, la definición de Fred::j_ puede no tener una asignación
the definition of Fred::j_ might not contain the = initializer part. Ver aquí y aquí para mas detalles.

¿Por qué las clases con miembros estáticos generan errores de compilador?

Por que los datos miembro estáticos deben ser explícitamente inicializados en un archivo fuente. Si usted no lo hace, es probable que obtenga un error de “undefined external” (variable externa no definida) por parte del enlazador, por ejemplo:

 // Fred.h
 
 class Fred {
 public:
   ...
 private:
   static int j_;   // Declara un dato miembro estático
   ...
 };

El enlazador (linker) le dirá que Fred::j_ no esta definido hasta que usted lo haga en algunos de sus archivos fuente.

 // Fred.cpp
 
 #include "Fred.h"
 
 int Fred::j_ = alguna expresión_que_debe_evaluarse_en_un_entero;
 
 // Alternativamente, si desea usted puede usar implícitamente 0 para variables estáticas enteras.
 // int Fred::j_;

El lugar habitual para definir los miembros de datos estáticos de la clase Fred es el archivo Fred.cpp (o Fred.c o cualquier otra extensión de archivo que sea considerado código fuente C/C++).

Nota: en algunos casos, puede inicializar las variables estáticas en la declaración de la clase, sin embargo, si ha usado alguna vez ese miembro de datos, usted aun tiene que definir de forma explícita en exactamente un archivo fuente. En este caso no se incluye un inicializador en la definición. Aquí un FAQ que cubre este tema.

¿Puedo agregar un inicializador a la declaración de una clase que tienen miembros estáticos constantes?

Sí, aunque con algunas consideraciones importantes.

Antes por las advertencias, he aquí un ejemplo sencillo donde sí que se permite:

 // Fred.h
 
 class Fred {
 public:
   static const int maximum = 42;
   ...
 };

Y, como otros miembros estáticos, éste necesita ser definido en exactamente un archivo fuente, pero esta vez sin inicializar la variable.

// Fred.cpp
 
 #include "Fred.h"
 
 const int Fred::maximum;
 
 ...

Las advertencias son que usted puede hacer esto sólo con los tipos int o enum, y que la expresión del inicializador debe ser una expresión que se puede evaluar en tiempo de compilación: sólo debe contener constantes, posiblemente en combinación con operadores. Por ejemplo, 3*4 es una expresión constante en tiempo de compilación, ya que es a*b siempre a y b son constantes en tiempo de compilación. Después de la declaración anterior, Fred::maximun es también una constante en tiempo de compilación: se puede utilizar en otros lugares en tiempo de compilación como si fuera una expresión contante.

Si alguna vez usamos la dirección de la variable constante estática Fred::maximun, como un paso por referencia o explícitamente diciendo y Fred::maximun, el compilador se asegurará de que tiene una dirección única. Si no es así, Fred::maximun ni siquiera tomará un espacio en el área estática de datos del programa.

¿Que significa el “fiasco de orden de inicialización de miembros estáticos”?

Una forma sutil de colgar tu programa.

El fiasco de orden de inicialización de variables estáticas es un aspecto muy sutil y mal entendida comúnmente en C++. Por desgracia, es muy difícil de detectar – los errores ocurren a menudo antes que el main() comience.

En pocas palabras, supongamos que tiene dos objetos estáticos x e y que existen en archivos de código fuente por separado, por ejemplo x.cpp e y.cpp. Supongamos además, que la inicialización del objeto y (por lo general el constructor del objeto y) hace una llamada a algún método en el objeto x.

Eso es todo. Es así de simple.

La tragedia es que usted tiene una probabilidad del 50%-50% de morir en el intento. Si el archivo fuente x.cpp pasa a ser inicializado en primer lugar, todo está bien. Pero el archivo fuente y.cpp se inicializa primero, y luego la variable y desea hacer algoantes de la inicialización de x estás frito. Por ejemplo, constructor y podría llamar a un método en el objeto x, sin embargo, el objeto x aún no ha sido construido.

Me han dicho que están contratando en McDonalds. Disfrute de su nuevo trabajo cocinando hamburguesas.

Si usted piensa que es “emocionante” jugar a la ruleta rusa con balas de verdad, puede dejar de leer aquí. Por otro lado si quiere mejorar sus posibilidades de supervivencia mediante la prevención de los desastres de una manera sistemática, es probable que quiera leer la siguiente pregunta.

Nota: El fiasco de la inicialización de orden estático también puede, en algunos casos, aplicar a tipos incorporados/intrínsecos del lenguaje.

¿Cómo puedo prevenir el “fiasco de orden de inicialización de miembros estáticos”?

Use el “constructor la primera vez”, lo que significa que simplemente envolver a su objeto estático en una función.

Por ejemplo, suponga que tiene dos clases, Fred y Barney. Y tenemos un objeto global de la clase Fred llamado x, y un objeto global de la clase Barney llamado y. Constructor de Barney invoca al método goBowling() del objeto x. El archivo x.cpp define el objeto x:

 // Archivo x.cpp
 # Include "Fred.h"
 Fred x;

El archivo y.cpp define el objeto y:

 // Archivo y.cpp
 # Include "Barney.h"
 Barney y;

Para completar el constructor Barney podría ser algo como esto:

 //Archivo Barney.cpp
 # Include "Barney.h"
 
 Barney: Barney ()
 {
   ...
   x.goBowling ();
   ...
 }

Como se describió anteriormente, el desastre se produce cuando el objetoy se construye antes que x, lo que puede pasar 50% de las veces por que las variables están en archivos fuente diferentes.

Hay muchas soluciones a este problema, pero una solución muy simple y totalmente portátil es para reemplazar el objeto global Fred x, con una función global, x() que devuelva el objeto de Fred por referencia.

 // File x.cpp
 
 #include "Fred.h"
 
 Fred& x()
 {
   static Fred* ans = new Fred();
   return *ans;
 }

Debido a que los objetos estáticos locales son construidos una sola vez desde su declaración, el código anterior new Fred(); solo será ejecutado una sola vez. Cualquier llamada posterior a la función x() devolverá el mismo objeto Fred apuntado por *ans, entonces todo lo que tiene que hacer es cambiar x por x().

Esto se conoce como construir el objeto al primer uso, ya que hace precisamente eso: el objeto global Fred se construye en su primer uso.

La desventaja de este enfoque es que el objeto Fred nunca se destruye. Existe otra técnica que responde a esta preocupación, pero es necesario para ser utilizado con precaución, ya que crea la posibilidad de otro problema igualmente desagradable.

Nota: El fiasco de orden de inicialización de variables estáticas también se puede, en algunos casos, aplicar a tipos de datos incorporados o intrínsecos.

C++  

Dejar una Respuesta