- En C++, la inicialización de objetos puede dividirse en default/value/list/aggregate initialization incluso con sintaxis parecidas como
T t;, T t{}; o T t{...}, lo que puede cambiar el estado real de los miembros
- Según dónde se escriba
= default para el constructor por defecto, cambia si ocurre o no zero-initialization; si se define fuera de la clase, los miembros pueden quedar con valores no inicializados
- Incluso un tipo que parece que tendría el constructor por defecto eliminado por tener un miembro
const int puede inicializarse con 0 mediante aggregate initialization si es un aggregate, como en A a{};
- La aggregate initialization basada en paréntesis de C++20 es más permisiva que la inicialización con llaves, por lo que puede generar diferencias como narrowing conversion y dangling references
- En la práctica, es más fácil evitar excepciones expresando la intención de inicialización con un constructor explícito, en lugar de depender de la combinación de constructores implícitos y reglas de inicialización
Dónde se bifurca la sintaxis de inicialización en C++
T t; realiza default-initialization
- Si
T es un tipo de clase y tiene un constructor por defecto, ejecuta ese constructor
- Si
T es un arreglo, default-initialize cada elemento
- En los demás casos, no hace nada
T t{}; parece value-initialization, pero por la sintaxis con llaves se procesa primero como list-initialization
- Incluso en ejemplos simples, la diferencia aparece de inmediato
int x{}; se value-initializa y queda inicializado en 0
int y; se default-initializa y su valor no queda inicializado
Pair p; y Pair q{};, con constructor por defecto, llaman al constructor
SimplePair r;, que solo tiene miembros, se default-initializa y sus miembros pueden quedar sin inicializar
El resultado cambia según dónde se declare el constructor por defecto
- Si el usuario no declara un constructor, el compilador declara un constructor por defecto implicitly-declared
- Si
T() = default; se escribe como primera declaración dentro de la clase, el estándar lo trata de forma casi similar a un constructor declarado implícitamente
- Si un constructor por defecto declarado implícitamente o explícitamente defaulted no se elimina, el compilador proporciona un implicitly-defined default constructor
- En términos de implementación, debe ser equivalente a
T() {} con cuerpo vacío y lista vacía de inicialización de miembros
- En el siguiente código,
t.x se vuelve 0
struct T {
int x;
T() = default;
};
T t{};
std::cout << t.x << std::endl;
- Esto se debe a que
t se value-initializa y, como el constructor por defecto de T no es user-provided, primero ocurre zero-initialization y luego se llama al constructor por defecto
= default fuera de la clase y constructores user-provided
- Aunque sea el mismo
= default, si se define fuera de la clase se convierte en un constructor user-provided
struct T {
int x;
T();
};
T::T() = default;
T t{};
std::cout << t.x << std::endl;
- En este caso,
T::T() = default; no fue defaulted en su primera declaración, sino un constructor definido como defaulted fuera de la clase
- Las reglas de value-initialization realizan default-initialization si existe un constructor por defecto user-provided o deleted
- Por lo tanto, en el ejemplo anterior solo se ejecuta el constructor por defecto sin zero-initialization y, como ese constructor no hace nada,
t.x queda con un valor basura
Casos en los que se elimina el constructor por defecto
- Si el compilador no puede crear un constructor por defecto razonable, el constructor por defecto declarado implícitamente puede definirse como deleted
- Algunas condiciones típicas son las siguientes
- Cuando hay un miembro de referencia no estático
- Cuando hay un miembro no estático o una clase base no abstracta que no puede default-construct o destruct adecuadamente
- Cuando hay un miembro
const no estático sin inicializador de miembro por defecto y ese miembro no es const-default-constructible
- Si el usuario proporciona directamente un constructor, el compilador no intenta definir un constructor por defecto implícito y tampoco crea un constructor deleted
- Incluso con estas condiciones, las reglas de inicialización de C++ se comportan en general de forma muy permisiva
El momento en que interviene aggregate initialization
struct A { const int x; }; A a{}; parece que no compilaría porque el constructor por defecto estaría eliminado, pero en realidad compila y a.x queda en 0
- La clave es que
A es un aggregate
- Un aggregate es un arreglo o una clase que cumple estas condiciones
- No tiene constructores user-declared ni heredados
- No tiene miembros de datos no estáticos directos private/protected
- No tiene clases base directas private/protected
- No tiene funciones virtuales ni clases base virtuales
- Cuando se realiza list-initialization sobre un aggregate, se aplica aggregate initialization, salvo excepciones específicas
- Cada elemento de la lista de inicialización inicializa, en orden, cada elemento del aggregate
- Si faltan elementos en la lista, los elementos restantes se inicializan con su inicializador de miembro por defecto, si lo tienen
- Si no tienen inicializador de miembro por defecto y no son referencias, se copy-initializan con una lista de inicialización vacía
- Por lo tanto,
A a{}; no es una llamada a constructor, sino un caso en el que a.x se copy-list-initializa con una lista vacía y, pasando por value-initialization, llega a zero-initialization
Reglas adicionales de la inicialización con llaves
- La sintaxis basada en llaves como
T t{...} y T t = {...} suele ser list-initialization
T t{...} es direct-list-initialization
T t = {...} es copy-list-initialization
- En tipos de clase que no son aggregates, si la lista de inicialización está vacía y hay un constructor por defecto, se realiza value-initialization
- En los demás casos, se consideran los constructores mediante la overload resolution normal, y las sobrecargas de constructores con
std::initializer_list tienen prioridad
- La inicialización con llaves no permite narrowing conversion al inicializar con un solo elemento
struct A {
const int x;
};
A b{4}; // posible
A c{4.0f}; // error de narrowing conversion
La inicialización con paréntesis es más permisiva
- La inicialización con paréntesis como
T t(a, b, c); invoca direct-non-list-initialization y, aunque se parece a direct-list-initialization, tiene reglas distintas
T t(); no se interpreta como creación de un objeto, sino como una declaración de función sin argumentos cuyo tipo de retorno es T, un problema conocido como most vexing parse
- Desde C++20, también se puede usar inicialización con paréntesis en aggregates
- La aggregate initialization basada en paréntesis se comporta de forma distinta a la basada en llaves
- Permite narrowing conversion
- Los elementos restantes se value-initializan directamente, no mediante inicialización con lista vacía
- No extiende la vida de los objetos temporales enlazados a referencias
struct T {
const int& r;
};
T t(42);
- En el código anterior,
t.r se convierte en una dangling reference y leerla es undefined behaviour
std::initializer_list, constructor de copia y elision
- La inicialización con paréntesis puede llamar al constructor de copia de
T incluso cuando existe una sobrecarga de constructor con std::initializer_list<T>
- En cambio, en la inicialización con llaves puede tener prioridad el constructor con
std::initializer_list
struct T {
T(std::initializer_list<T>) {
std::cout << "list" << std::endl;
}
T(const T&) {
std::cout << "copy" << std::endl;
}
};
T t{}; // list
T s{t}; // list
T r(t); // copy
T q(T{}); // list, sin copia
- Que no ocurra copia en
T q(T{}); se debe a la regla según la cual, en direct- y copy-initialization, si el initializer es un prvalue de tipo T, el objeto se inicializa directamente con esa expresión
- Este comportamiento suele conocerse como copy elision, pero el estándar no lo llama explícitamente elision
- No hay suficiente explicación en el estándar sobre si la misma elision es posible en list-initialization, y CWG issue 2311 está relacionado
- GCC y Clang realizan elision en la mayoría de los casos
- Si existe una sobrecarga de constructor con
std::initializer_list<T>, GCC usa ese constructor en lugar de hacer elision
Orden de evaluación y conclusión práctica
- El orden de evaluación de los elementos de una lista de inicialización con paréntesis no está garantizado
- Las listas de inicialización con llaves evalúan estrictamente los elementos de izquierda a derecha
- También existen reglas especiales como la inicialización de variables estáticas y constant initialization, pero solo con la inicialización de objetos comunes las reglas ya son suficientemente complejas
- En la práctica, es más seguro escribir un constructor explícito para dejar clara la intención de inicialización, en vez de depender de constructores por defecto implícitos y reglas de inicialización
1 comentarios
Comentarios en Hacker News
La explicación de que al inicializar por valor
Tel resultado es 0 era correcta; al principio se me pasó, pero el autor realmente estaba inicializandoxpor valorEn general, las reglas de inicialización de C++ se han vuelto enrevesadas y tienen partes poco intuitivas tras 40 años de expansiones y mantenimiento, pero diría que en un 99.9% de los casos funcionan como uno esperaría
Creo que una gran mejora sería hacer explícita la inicialización por defecto y que todo lo demás siempre hiciera inicialización por valor. Cuando solo quieras evitar el costo de llenar de 0 un arreglo grande, sería mejor indicarlo explícitamente con una sintaxis como
std::array = void;Parece un caso de alguien que quiso pasarse de listo y luego se queja de que es difícil de manejar
Todo el enfoque está mal. No metas una referencia const dentro de un struct; si de verdad lo necesitas, lo correcto sería usar
std::reference_wrapperLa respuesta es un poco seca, pero el punto clave es que la conclusión del artículo está completamente equivocada. No uses constructores escritos a mano y sigue la regla de 5/3/0; y si necesitas conservar una referencia const, entonces verifica si estás pasando un temporal rvalue. No es un problema tan aterrador
Incluso alguien que solo haya pasado por un tutorial básico simplemente evita este tipo de escenarios artificiales
std::reference_wrapper, y tampoco lo he visto en varias bases de código C++ en las que he trabajadoTal vez se use en algún rincón de código de plantillas profundo y complejo, pero aunque pueda ser cierto, en mi experiencia no es un conocimiento de sentido común
El texto original de
I Have No Mouth, and I Must Scream (1967)puede leerse aquí: https://talesofmytery.blogspot.com/2018/10/harlan-ellison-i-...La familia de Martin H Greenberg se estaba quedando en mi casa, y él estaba buscando a Martin. Me gustaba la ciencia ficción y había leído varios de sus libros, pero después de oír su nombre casi recuerdo la conversación como algo borroso. Al final desperté a Martin y le pasé la llamada, y después me costó volver a dormirme
Me sorprende que no haya comentarios sarcásticos sobre la parte donde
unlessestá incluso en negritasHay sintaxis elegante como
T t{v0};yT t{v0, v1};, pero la inicialización con un solo elemento no funciona de forma tan confiable como cuando hay dos o más elementosEn un lenguaje que avanza hacia hacer más fácil construir structs con parameter packs y que incluso soporta algo parecido a arreglos de longitud variable inicializables de esta forma, esta diferencia es rara. El tipo, por supuesto, también podría ser una plantilla
Puedes usar constructores directos o inicializar una tupla o un arreglo suministrando un solo elemento, pero en casos especiales podría llamarse un constructor equivocado. Cuando descubrí esto justo después de que salieran las listas de inicialización de C++11, pensé que era una locura
Para ver todavía más rarezas de C++, recomiendo el C++ FQA: https://yosefk.com/c++fqa/
Ya tiene como 15 años, pero C++ casi nunca elimina funciones o comportamientos viejos, así que al mismo tiempo no envejece
El tema del blog es realmente hermoso. Claramente parece inspirado en las computadoras de la era DEC, pero al mismo tiempo se ve limpio y minimalista, lo cual se siente fresco
Horrible. Es una de las partes aterradoras de C++. Tiene funciones geniales y es un lenguaje en el que trabaja gente brillante, así que da más pena
Espero que algo como C++ syntax 2 de Herb lo convierta en un lenguaje que gente común como yo también pueda usar
Los ejemplos del artículo están hechos para tocar deliberadamente casos límite de la función del lenguaje que genera automáticamente, como excepción, funciones miembro especiales que el programador decidió no escribir por su cuenta
C++ tiene como objetivo de diseño que solo pagues por lo que usas, y la regla de 3 / regla de 5 es conocimiento básico de nivel introductorio en C++. También forma parte de lo básico saber que estas funciones miembro especiales solo las genera el compilador bajo ciertas condiciones, así que si no se cumplen, no se generan automáticamente
Al final, no me parece tan absurdo saber que debes definir por tu cuenta el constructor que vas a usar. Tampoco debería sorprender que haya que inicializar al crear una instancia. En la práctica, la gente trabaja así todos los días sin hacer tanto escándalo
Leer esto marea. Me recordó cuando intentaba entender los constructores de Java y la inicialización de objetos
Al menos en mi experiencia hasta ahora, la elección de no tener constructores especiales, como en Go y Rust, hace que muchas cosas sean más simples
Me pregunto si alguien, después de pasarse a un lenguaje sin constructores, realmente los ha extrañado
Claro, puedes ir escribiendo los campos uno por uno manualmente con
ptr::writesobreMaybeUninit, pero no es ergonómico y la estructura tiene que permitirlo explícitamenteAl final eso significa que hay que declarar y definir explícitamente todos los constructores, y C++ también ofrece esa opción desde el principio. La generación automática de funciones miembro especiales es una función añadida para que, en casos muy específicos, la gente que quiere evitar código repetitivo pueda hacerlo
Si quieres, puedes agregar tus propios constructores y vivir tranquilamente. Quejarse de que las funciones miembro especiales vuelven complejas las cosas simples es parecido a decir que el inglés no es simple porque el diccionario tiene palabras difíciles
Según entiendo, ¿los constructores de Rust no son básicamente parecidos a los de Java?
https://codefibershq.com/blog/golang-why-nil-is-not-always-n...
Me pregunto si existe alguna herramienta de C++ que agregue o muestre todas las acciones implícitas que ocurren detrás de escena
Por ejemplo, una herramienta que muestre constructores añadidos automáticamente, constructores de copia implícitos y otras sorpresas similares
cppinsightsEn el caso de
T::T() = default;, uno esperaría que el resultado impreso fuera 0, pero el ejemplo donde termina siendo un valor basura en realidad no es tan extrañoEnlace relacionado: https://consteval.ca/2024/07/03/initialization/#:~:text=You%...
Porque si se hiciera de otra manera, cualquier usuario de la biblioteca podría cambiar el comportamiento de la biblioteca