HOWTO · C++

Constructores vacíos en C++

Comprende qué significa un constructor con el cuerpo vacío en C++, cómo se relaciona con los constructores predeterminados y cómo inicializar los miembros de forma segura.

En esta página

Un «constructor vacío» suele referirse a un constructor escrito por el usuario cuyo cuerpo está vacío. Es una expresión habitual, no una categoría distinta del lenguaje C++. La pregunta clave es si una clase tiene un constructor predeterminado: uno que se puede llamar sin argumentos. Ese constructor puede tener parámetros con argumentos predeterminados, así que «constructor predeterminado» no significa necesariamente que se haya declarado con una lista de parámetros vacía.

Un constructor con el cuerpo vacío no asigna automáticamente un valor útil a cada miembro. Las reglas de inicialización de los miembros dependen de cómo se crea el objeto y de las declaraciones de la clase. Cuando la clase necesite un estado inicial fiable, usa un inicializador de miembro dentro de la clase o una lista de inicializadores de miembros.

Constructores vacíos y constructores predeterminados

Un constructor predeterminado es cualquier constructor que se puede llamar sin argumentos. Por ejemplo, Gauge(int level = 1) se puede llamar sin argumento, así que es un constructor predeterminado. Un constructor con el cuerpo vacío, como Gauge() {}, también es un constructor predeterminado, pero un constructor puede realizar una inicialización útil en su cuerpo o en su lista de inicializadores de miembros.

C++ puede declarar implícitamente un constructor predeterminado cuando una clase no tiene constructores declarados por el usuario. En cuanto declaras otro constructor, el compilador no te proporciona también un constructor implícito sin argumentos. Si los objetos deben poder construirse sin argumentos, declara esa sobrecarga explícitamente o, cuando corresponda, solicita la forma generada por el compilador con = default. Un constructor marcado como predeterminado y uno con el cuerpo vacío son declaraciones distintas: el primero pide al compilador que genere la operación; el segundo es una función escrita por el usuario cuyo cuerpo no hace nada.

La guía relacionada sobre constructores predeterminados explica las reglas generales de esta categoría. Aquí nos centramos en el cuerpo vacío y en lo que implica para los miembros de un objeto.

Definir un constructor vacío

Un constructor sin argumentos tiene el mismo nombre que su clase y no tiene tipo de retorno. En el programa siguiente, value recibe un valor inicial dentro de la clase. Al crear meter, se llama al constructor, y la última línea escribe el miembro en la salida estándar. El inicializador dentro de la clase se ejecuta como parte de la construcción del objeto, antes que el cuerpo del constructor. Este programa completo usa únicamente funciones estándar de C++ y se puede compilar por sí solo. A continuación se muestra la salida observada.

Ejecuta este ejemplo:

El código y las etiquetas o salidas visibles de esta sección se mantienen en inglés.

#include <iostream>

struct Meter {
    int value{7};
    Meter() {}
};

int main() {
    Meter meter;
    std::cout << meter.value << '\n';
}
7

La declaración Meter() {} tiene el cuerpo vacío; value{7} inicializa el miembro antes de que se ejecute ese cuerpo. Al crear meter, se llama al constructor sin argumentos y el programa escribe el valor del miembro. Este ejemplo distingue la función del constructor de la fuente de inicialización del miembro.

También puedes inicializar miembros en una lista de inicializadores de miembros, por ejemplo, Meter() : value(7) {}. Esto es necesario para los miembros que no se pueden asignar después de la construcción, como los miembros de referencia y los miembros de datos const, y suele ser el lugar más claro para establecer invariantes. Los miembros se inicializan en el orden en que se declaran en la clase, independientemente del orden en que aparezcan sus nombres en la lista.

Qué ocurre con los miembros sin inicializadores

El cuerpo vacío de un constructor no inicializa los miembros escalares. Si un escalar como int count; no tiene un inicializador dentro de la clase ni aparece en la lista de inicializadores de miembros, la inicialización predeterminada de un objeto deja ese miembro sin un valor inicializado. No es seguro leerlo antes de asignarle un valor. No dependas de un setter posterior, a menos que la clase impida cualquier lectura antes de que se llame a ese setter.

Un miembro omitido usa su inicializador predeterminado de miembro, si lo tiene. De lo contrario, se inicializa según su tipo; si es de tipo clase, se inicializa de forma predeterminada seleccionando un constructor predeterminado. Esto no significa que todos esos miembros reciban valores específicos de la aplicación. Comprueba el tipo y los requisitos de inicialización de cada miembro en lugar de suponer que un cuerpo vacío deja el objeto completamente inicializado.

La inicialización predeterminada y la inicialización por valor son formas distintas de inicialización. Para un objeto de tipo clase, la inicialización predeterminada llama al constructor predeterminado seleccionado. La inicialización por valor puede primero inicializar el objeto con ceros cuando el constructor predeterminado seleccionado no está proporcionado por el usuario y, después, realizar la inicialización predeterminada. Un constructor escrito por el usuario con {} sí está proporcionado por el usuario, así que inicializar esa clase por valor no vuelve seguro un miembro escalar que de otro modo no estaría inicializado. Estas reglas son fáciles de interpretar mal; un inicializador dentro de la clase o una lista de inicializadores de miembros expresa el estado deseado de forma más fiable.

Por ejemplo, struct Sample { int count{}; Sample() {} }; da a count el valor cero mediante su inicializador dentro de la clase. En cambio, struct Sample { int count; Sample() {} }; no inicializa count por el mero hecho de que exista el constructor. Las llaves del cuerpo vacío de una función no son inicializadores de miembros.

Cuándo resulta útil un constructor vacío

Un constructor con el cuerpo vacío es razonable cuando la clase no tiene que realizar ninguna tarea durante la construcción y cada miembro ya cuenta con una regla de inicialización adecuada. También puede hacer disponible deliberadamente una operación de construcción pública para un framework o un contenedor que requiera construcción predeterminada. En ambos casos, la clase debe ser válida inmediatamente después de construirse, en vez de depender de que quienes la usan recuerden una secuencia de llamadas a setters.

Prefiere los inicializadores de miembros dentro de la clase para los valores predeterminados sencillos que comparten varios constructores. Usa una lista de inicializadores de miembros cuando el valor inicial dependa de argumentos del constructor, al inicializar miembros const o de referencia, o cuando el tipo de un miembro requiera un constructor específico. Si la preparación requiere validación, adquirir recursos o establecer una invariante de la clase, realiza ese trabajo en un constructor adecuado en vez de dejar vacío su cuerpo.

Según la operación, las colecciones y los arreglos pueden requerir que sus elementos se puedan construir de forma predeterminada. Un constructor explícito sin argumentos puede satisfacer ese requisito, pero por sí solo no hace segura la inicialización diferida. Considera si una fábrica, los argumentos del constructor o una operación del contenedor que construya los elementos directamente en su ubicación comunican mejor el estado requerido.

Errores comunes y alternativas más seguras

No equipares «cuerpo vacío» con «todos los miembros reciben valores predeterminados». Durante la inicialización predeterminada, un escalar sin inicializador puede quedar indeterminado. Asígnale un inicializador dentro de la clase, inicialízalo en la lista de inicializadores de miembros o exige el valor mediante parámetros del constructor.

No supongas que declarar un constructor con parámetros deja disponible un constructor sin argumentos. Si necesitas ambas formas, declara las dos o marca explícitamente como predeterminada la forma sin argumentos cuando el comportamiento generado sea adecuado. La disponibilidad de los constructores depende de las declaraciones y de las reglas del lenguaje, no del orden en que aparezcan en la clase.

Por último, evita crear un objeto en un estado incompleto para rellenarlo después mediante setters. Si la clase necesita ciertos datos, recíbelos en el constructor y establece allí la invariante. Deja vacío el cuerpo del constructor solo cuando esa ausencia de operaciones sea intencional y las propias declaraciones de los miembros proporcionen un estado inicial válido.