Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


 Prev.

4.11.2d3   Iniciar miembros  (II)

§6  Iniciadores

El sistema de argumentos y sentencias de asignación descritos en la página anterior, tiene serios problemas. Por ejemplo, ¿Qué hacer si un miembro es constante?. Supongamos el caso siguiente, que sigue las pautas anteriores:

class X { 
  char c;
  const int k;
  public:
  X(char caracter, int kte) {
    c = caracter;
    k = kte;      // Error: asignar a constante!!
  };
};

Este problema no solo se presenta con miembros constantes. También cuando un miembro es instancia de una clase que no tiene constructor por defecto, o es una referencia a un objeto. Son los casos resumidos en el siguiente código:

class A {
  int i; char c;
};

class B {
  int ib; char cb; 
  const int k;
  A a;
  A& ar;
};

Desde luego es posible definir un constructor de la clase B que inicie adecuadamente los miembros ib y cb. Pero con los medios tradicionales no es posible inicializar el resto de miembros: la constante k; la instancia a de A (que no tiene constructor por defecto que inicialice adecuadamente este objeto), ni a la referencia ar ( 4.2.3).

Para resolver este problema se utiliza un recurso denominado lista de iniciadores, que adopta el aspecto del ejemplo que sigue.

class A {
  int i; char c;
};

class B {
  int ib; char cb; 
  const int k;
  A a;
  public:
  // <----- argumentos -------> : <-- lista-de-iniciadores --->
  B (int ent, char car, int kte): k(kte), cb(car), ib(ent), a() {
     ...   // cuerpo del constructor (bloque de su definición)
  };
};


Como puede verse, la lista de iniciadores es un extraño artificio sintáctico utilizado por los diseñadores de C++ como un añadido de los constructores para solventar problemas que de otra forma, violarían reglas de sintaxis bien establecidas (por ejemplo asignación a constantes).

Nota:  no confundir esta lista de iniciadores incluida en el constructor durante el diseño de la clase, con la lista de iniciadores señalada antes que se utiliza como lista de argumentos que pasan al constructor para instanciar objetos de la clase.


§6.1  Los iniciadores siguen ciertas reglas de sintaxis:

  • Aparecen entre la lista de parámetros formales del constructor y el bloque de su definición.
  • Están precedidos de dos puntos ( : ) y separados entre sí por comas ( , ).
  • Adoptan la misma forma que las funciones, donde el nombre de la función sería el del miembro a iniciar y el argumento, el correspondiente de la lista de parámetros formales del constructor (aunque puede ser otro valor).
  • Un miembro de la lista puede adoptar la forma de función con más de un argumento. Por ejemplo:

    C(int n, char c, int k): kte(k), ch(c, n) { ... };

  • Si un iniciador no necesita argumento puede omitirse de la lista. El constructor de la clase anterior sería equivalente a:

    B(int ent, char car, int kte): k(kte), cb(car), ib(ent) { };


§6.2  Conceptualmente las asignaciones implícitas en la lista de iniciadores se efectúan antes que se ejecute el constructor, y por tanto antes que el resto de sentencias contenidas en él (esta circunstancia es aprovechada en ocasiones para algunas técnicas de programación avanzada). Considere detenidamente el resultado de los dos ejecutables:

#include <iostream>
using namespace std;

struct C {
  int x;
  void show() { cout << "Valor de x: " << x << endl; }
  C (int n = 33) {     // L7: Constructor por defecto
    show();
    x = n;
    show();
  }
};

int main() {   // ========
  C c1;
  return 0;
}

Salida:

Valor de x: 6758844    // basura!!
Valor de x: 33

Después de cambiar la sentencia L7 por

C (int n = 33) : x(1) {   // L7bis

se obtiene el siguiente resultado:

Valor de x: 1
Valor de x: 33


§6.3  La lista de iniciadores puede utilizarse con parámetros del constructor que tengan valores por defecto.

#include <iostream>
using namespace std;

class C {
  char ch;
  public:
  C (char c = 'X') : ch(c) {    // Constructor
    if (ch == 'X') cout << "Valor por defecto  ";
    else cout << "Recibido argumento ";
    cout << " .Valor actual: " << ch << endl;
  }
};

int main(void) {   // =========
  C c1;            // invocación implícita al constructor por defecto
  C c2('A');       // invocación implícita al constructor con argumentos
  C c3 = { 'B' };  // variación sintáctica de la anterior
  C c4 = C('C');   // invocación explícita al constructor con argumentos
  return 0;
}

Salida:

Valor por defecto  .Valor actual: X
Recibido argumento .Valor actual: A
Recibido argumento .Valor actual: B
Recibido argumento .Valor actual: C


§6.4  Recordar que, con independencia del orden utilizado por el programador para la lista de iniciadores, el orden de iniciación de los miembros coincide con el orden en que estos aparecen en el cuerpo del constructor. La razón es que con independencia del orden utilizado en el fuente, el compilador transforma la lista para que coincida con el orden de declaración de los miembros de la clase [1].

Generalmente este cambio no tiene importancia práctica, pero pueden darse circunstancias en que no sea así. El programa del ejemplo funciona correctamente y proporciona la salida indicada, porque en este caso, el orden de inicio está determinado en el cuerpo del constructor C-1:

#include <iostream>
using namespace std;

struct C {
  char* cptr;
  int size;
  C(unsigned int n = 2) {   // constructor C-1
    size = (n < 27 ? n : 1);
    cptr = new char [size];
    for (int i=0 ; i<size ; i++) { cptr[i] = i+65; }
  }
  ~C() { delete[] cptr; }
};

void main() {   // ===============
  C c1(12);
  cout << "Miembros: ";
  for (int i=0 ; i<c1.size ; i++) {
    cout << "[" << c1.cptr[i] << "], ";
  }
}

Salida:

Miembros: [A], [B], [C], [D], [E], [F], [G], [H], [I], [J], [K], [L],

Un nuevo diseño del constructor C-1, pasando la asignación del miembro size a una lista de iniciadores, no afecta el resultado:

C(unsigned int n=2): size(n < 27 ? n : 1) {  // constructor C-2
  cptr = new char [size];
  for (int i=0 ; i<size ; i++) { cptr[i] = i+65; }
}

Sin embargo, un intento de pasar también la asignación de cptr a la lista de iniciadores, origina resultados indefinidos que pueden dar lugar a errores de runtime (observe que se ha definido antes cptr que size):

C(unsigned int n=2): size (n < 27 ? n : 1), cptr = new char [size] { // C-3
   for (int i=0 ; i<size ; i++) { cptr[i] = i+65; }
}


§6.5  En el siguiente ejemplo mostramos la utilización de la lista de iniciadores para iniciar una constante, y un miembro que es a su vez instancia de otra clase:

#include <iostream>
using namespace std;

class A {
  public: int x, y;
  A(int i, int j) { x = i+1, y = j+2; }  // Constructor A
};

class B {
  public:
  class A a;
  const int k1;
  B(int i=0) : k1(i), a(i+20, i) {       // Constructor B
    cout << "Constructor-B" << endl;
  }
};

int main() {                // ==========
  B b(10);                  // invocación implícita al constructor B
  cout << b.k1 << endl;     // Salidas de comprobación
  cout << b.a.x << endl;    // Observe la notación para acceso a
  cout << b.a.y << endl;    // miembro-de-miembro
}

Salida:

Constructor-B
10
31
12

Comentario

En este ejemplo puede comprobarse como el término a(i+20,i) de la lista de iniciadores del constructor B() actúa como una invocación, con argumentos, al constructor A(). Tenga en cuenta que esta sintaxis exige la existencia de un constructor explícito en la clase .

Observe que un término de la lista de iniciadores puede funcionar como una función con varios argumentos. En este caso el término a tiene dos argumentos: i+10 e i.


§6.6  La iniciación del constructor de la subclase de una jerarquía puede incluir al constructor de la superclase. Sin embargo, no puede ser utilizada para miembros heredados; solo pueden ser iniciados de esta forma los miembros privativos ( 4.11.2b).  Considere la sintaxis del ejemplo:

#include <iostream>
using namespace std;

class B {        // Superclase
  public:
  int x:
  const int k1;
  B(int i=0) :k1(i+1) { cout << "Constructor-B" << endl; }
};

class D : public B {      // Subclase
  public:
  int k2;
  D(int i = 0) :B(i+10),  // L12: Ok.
                x(2),     // Error!! x es miembro heredado
                k2(i)     // Ok.
    { cout << "Constructor-D" << endl;}
};

int main() {      // ===============
  D d(5);                 // invocación implícita a constructor D
  cout << d.k1 << endl;   // Comprobación
  cout << d.k2 << endl;
}

Salida (después de eliminado el iniciador erróneo):

Constructor-B
Constructor-D
16
5

En este caso, el término B(i+10) de la lista de iniciadores actúa como una invocación al constructor del espacio de nombres B del objeto d. También aquí se exige la existencia de un constructor explícito en la superclase B (). Observe como el constructor de clase-base es invocado antes que el de la clase derivada ( 4.11.2d1).

Nota: la sentencia L12:

D(int i = 0) :B(i+10), k2(i) { /* ... */}

puede ser expresada también

D(int i = 0) : (i+10), k2(i) { /* ... */}

En esta caso se sobreentiende que el argumento (i + 10) se refiere al constructor de la superclase [3].

§6.7  Algunos problemas

El código del ejemplo anterior  ( &6.5), compila sin dificultad. Sin embargo, haciendo una pequeña modificación, en el sentido de que la clase B derive de una superclase C. Se presentan algunos problemas:

#include <iostream>
using namespace std;

class A {
  public: int x, y;
  A(int i, int j) { x = i+1, y = j+2; }  // Constructor A
};

class C {
  public: int m, n;
  C(int i, int j) { m = i+2, n = j+3; }  // Constructor C
};

class B : public C {
  public:
  class A a;
  const int k1;
  B(int i=0) : k1(i), a(i+20, i) {      // Constructor B (Lin.18)
    cout << "Constructor-B" << endl;
  }
};

int main() {    // ==========
  B b(10);      // invocación implícita al constructor B
  cout << b.k1 << endl;   // Salidas de comprobación
  cout << b.a.x << endl;
  cout << b.a.y << endl;

}

Con este diseño, el compilador GNU G++ 3.4.2-20040916-1 para Windows produce los siguientes errores de compilación:

In constructor `B::B(int)': 
18: no matching function for call to `C::C()' 
   candidates are: C::C(const C&) 
                   C::C(int, int)

Por su parte, Borland C++ 5.5 para Win32 muestra el siguiente resultado:

Error E2251 18: Cannot find default constructor to initialize base class
'C' in function B::B(int)
...


En realidad, el problema no está motivado por la presencia de los iniciadores del constructor (hemos visto en el ejemplo original que funcionan correctamente), sino en la iniciación de los miembros C::m y C::n de B heredados de C [4].  Estos miembros no pueden ser iniciados porque la superclase C no dispone de un constructor por defecto (el hecho de que hayamos definido un constructor explícito ha evitado que el compilador incluya uno de oficio).

La solución puede ser añadir un constructor por defecto (que pueda ser invocado sin argumentos).  El más simple podría ser un constructor nulo (que en realidad no hace nada).  El diseño de C quedaría como sigue:

class C {
  public: int m, n;
  C() { }            // constructor por defecto
  C(int i, int j) { m = i+2, n = j+3; } // Constructor explícito
};

La nueva versión compila sin errores, pero el problema es que los miembros C::m y C::n  de los objetos B no serán inicializados correctamente (contendrán basura), lo que en ocasiones no es deseable ni conveniente.  Aprovechado que la superclase C tiene un constructor (aunque no sea por defecto), podemos conseguir una mejor solución incluyendo un iniciador que se refiera a los miembros heredados.  Para ello, dejamos la definición de la clase C con su diseño original, y modificamos el constructor de B, queda como sigue:

B(int i=0) : k1(i), C(0, 0), a(i+20, i) {   // Constructor B-bis
  cout << "Constructor-B" << endl;
}

Hemos indicado que el iniciador C(0, 0) equivale a una invocación al constructor de la superclase con los argumentos correspondientes.  Observe que hemos utilizado 0 como valor para los argumentos, pero podrían haber sido otros cualquiera.  Incluso dependientes del argumento i del constructor de la subclase.  Los valores de inicio de estos miembros pueden ser inspeccionados añadiendo dos sentencias al cuerpo de main:

cout << b.m << endl;   // -> 2
cout << b.n << endl;   // -> 3

§6.8  En la práctica

La justificación y génesis de la lista de iniciadores es la que se ha expuesto. Sin embargo, una vez establecido el mecanismo, es muy frecuente su utilización para iniciar miembros que no son constantes, ni instancias de otras clases, ni referencias.  Es decir, su utilización con miembros que técnicamente no requieren del artificio. Como señalábamos al tratar de construcción y destrucción de objetos ( 4.11.2d), algunos autores recomiendan que  la iniciación de los miembros escalares, cualquiera que sea su tipo, se realice siempre en la lista de iniciadores, dejando el cuerpo del constructor para cualquier lógica adicional que sea necesaria durante la construcción. Como también señalamos al tratar de las reglas de buena práctica ( 4.13.1), esta opinión es compartida por Scott Meyers, que entre otros consejos, incluye el de preferir la iniciación de miembros (en la lista de iniciadores) antes que su asignación directa en el cuerpo del constructor.

Nota:  el compilador GNU gcc incluye la opción -Weffc++ que avisa si se transgrede alguno de los consejos de Meyers, por lo que entre otras, avisa si en el diseño de los constructores no se respeta la mencionada recomendación.

Las razones son varias, incluyendo la argumentación de que se mejora la legibilidad del código; la concisión, o tal vez cuestión de gusto personal.  La consecuencia es que es frecuente encontrar definiciones de clases en las que el cuerpo del constructor está vacío { }, agrupándose las iniciación de miembros en la lista de iniciadores.

Sin embargo prescindiendo de consideraciones de tipo estético, en algunas circunstancias esta sintaxis podría estar perfectamente justificada y suponer una mejora de la seguridad y del rendimiento.  El hecho de que las asignaciones implícitas en la lista de iniciadores se efectúen antes que se ejecute el cuerpo del constructor (antes que el resto de sentencias contenidas en él) puede ser aprovechada si hay que incluir mecanismos de manejo de excepciones en el constructor, ya que elimina la innecesaria creación y subsiguiente destrucción de objetos que puede ocurrir si la iniciación de miembros se realiza en el cuerpo del constructor.

§6.9  Ejemplos adicionales

class C {
  int x;
  char* str;
  A a;
  public:
  C () {                 // constructor por defecto
    x = 0; str = NULL; a = A();
  }
  C (int i, char* ch) {  // constructor explícito
    x = i;  str = ch;
  }
};

es muy frecuente que encontremos la siguiente alternativa al diseño anterior:

class C {
  int x;
  char* str;
  public:
  C () : x(0), str(NULL), a(A()) {}      // constructor por defecto
  C (int i, char* ch): x(i), str(ch) {}  // constructor
};

Incluso es posible utilizarlo con argumentos que tengan valores por defecto:

class C {
  char ch;
  public:
  C(char c = '\0') : ch(c) {}     // constructor
};

También puede utilizarse:

class C {
  char ch;
  public:
  C() : ch('\0') {}     // Constructor
};

Otro ejemplo:

class C {
  public:
  size_t size;
  char* string;
  C (size_t n=0): size(n), string( new char[size] ) {}  // constructor
  ...
};

§7  Los tipos básicos

Una cuestión que inicialmente puede resultar extraña (aunque meditada un poco más no lo es tanto), es que los denominados tipos básicos, escalares o primitivos ( 2.2), pueden ser considerados también como clases. Serían unas clases preconstruidas en el lenguaje a las que no podemos modificar su diseño. Generalmente su utilización sigue la sintaxis ya conocida, pero se les puede aplicar igualmente la sintaxis utilizada con las clases. Por ejemplo, en C++ son perfectamente válidas las siguientes sentencias:

int x = int();            // L1:
int y = int(5);
char c = char();          // L3:
char d('Z');              // L4:
char e = char('X');       // L5:
float f = float(1.23);

La expresión L1 equivale a:

int x = int(0);

mientras que la expresión L3 equivale a:

char c = char(´\0´);

Igualmente les puede ser aplicada la sintaxis utilizada con los operadores new y delete para los tipos abstractos:

int* iptr = new int;
const char* cptr = new char('Z');
delete cptr;
delete iptr;

No obstante, salvo estas últimas expresiones, que podrían ser utilizadas para construir estos objetos en memoria persistente, la sintaxis anterior no es habitual para iniciar tipos básicos. Es desde luego más frecuente encontrar

char d = 'Z';

que expresiones como L4 o L5.

  Inicio.


[1]  El compilador GNU cpp dispone de una opción de compilación específica: -Wreorder, que advierte cuando el orden de los miembros de la lista de iniciadores no coincide con el orden en que deben ser construidos (orden de aparición en la definición).  Por ejemplo:

class A {
  public:
  int i;
  int j;
  A(): j(0), i(1) { }
};

En cualquier caso, con independencia de la existencia de esta opción, el compilador siempre reordena la lista para que corresponda con la prevista en la declaración.

[3]    Stroustrup & Ellis:  ACRM  §18.3.2

[4]  Como puede verse, en esta ocasión del mensaje de error de Borland es mucho más explícito que el de GNU. 

 Prev.