15.10. Funciones virtuales y constructores

Cuando se crea un objeto que contiene funciones virtuales, su VPTR debe ser inicializado para apuntar a la VTABLE correcta. Esto debe hacerse antes de que exista la posibilidad de llamar a una función virtual. Como se puede adivinar, debido a que el constructor tiene el trabajo de dar existencia al objeto, también será trabajo del constructor inicializar el VPTR. El compilador de forma secreta añade código al principio del constructor para inicializar el VPTR. Y como se describe en el Capítulo 14, si no se crea un constructor de una clase de forma explícita, el compilador genera uno de forma automática. Si la clase tiene funciones virtuales, el constructor generado incluirá el código apropiado para la inicialización del VPTR. Esto tiene varias consecuencias.

La primera concierne a la eficiencia. La razón de que existan funciones inline es reducir la sobrecarga que produce llamar a funciones pequeñas. Si C++ no proporcionara funciones inline, se podría usar el preprocesador para crear esas «macros». Sin embargo, el preprocesador no tiene el concepto de acceso ni el de clase, y por tanto no podría usarse para crear macros de funciones miembro. Además, con los constructores que deben tener código oculto insertado por el compilador, una macro del preprocesador no funcionaría en absoluto.

Cuando se estén buscando agujeros de eficiencia, hay que tener en cuenta que el compilador está insertando código oculto en el constructor. No sólo debe inicializar el VPTR, sino que también debe comprobar el valor de this (por si el operador new devuelve cero) y llamar a los constructores de la clase base. En conjunto, este código puede tener cierto impacto en lo que se pensaba que era una pequeña llamada a una función inline. En particular, el tamaño del constructor puede superar el ahorro que se consigue al reducir la sobrecarga en las llamadas. Si se hacen un montón de llamadas a constructores inline, el tamaño del código puede crecer sin ningún beneficio en la velocidad.

Por supuesto, probablemente no convertirá de inmediato todos los constructores pequeños en funciones no inline, porque es mucho más fácil escribirlos inline. Pero cuando esté afinando el código, recuerde considerar la posibilidad de quitar los constructores inline.

15.10.1. Orden de las llamadas a los constructores

La segunda faceta interesante de los constructores y las funciones virtuales tiene que ver con el orden de las llamadas a los constructores y el modo en que las llamadas virtuales se hacen dentro de los constructores.

Siempre se llama a todos los constructores de la clase base en el constructor de una clase heredada. Esto tiene sentido porque el constructor tiene un trabajo especial: asegurarse de que el objeto se construye de forma apropiada. Una clase derivada sólo tiene acceso a sus propios miembros, y no a los de la clase base. Únicamente el constructor de la clase base puede inicializar de forma adecuada sus propios elementos. Por lo tanto es esencial que se llame a todos los constructores; de otra forma el objeto completo no estaría construido de forma adecuada. Por eso el compilador obliga a hacer una llamada a un constructor por cada parte de una clase derivada. Se llamará al constructor por defecto si no se hace una llamada explícita a un constructor de la clase base en la lista de inicialización del constructor. Si no existe constructor por defecto, el compilador se quejará.

El orden de las llamadas al constructor es importante. Cuando se hereda, se sabe todo sobre la clase base y se puede acceder a todos los miembros públicos y protegidos (public y protected) de la clase base. Esto significa que se debe poder asumir que todos los miembros de la clase base son válidos cuando se está en la clase derivada. En una función miembro normal, la construcción ya ha ocurrido, por lo que todos los miembros de todas las partes del objeto ya han sido construidos. Dentro del constructor, sin embargo, hay que poder asumir que todos los miembros que se usen han sido construidos. La única manera de garantizarlo es llamando primero al constructor de la clase base. Entonces, cuando se esté en el constructor de la clase derivada, todos los miembros a los que se pueda acceder en la clase base ya habrán sido inicializados. «Saber que todos los miembros son válidos» dentro del constructor es también la razón por la que, siempre que sea posible, se deben inicializar todos los objetos miembro (es decir, los objetos puestos en la clase mediante composición) en la lista de inicialización del constructor. Si se sigue esta práctica, se puede asumir que todos los miembros de la clase base y los objetos miembro del objeto actual han sido inicializados.