c++ initialization language-lawyer

Inicialización cero de C++-¿Por qué no se inicializa `b` en este programa, pero se inicializa` a`?



initialization language-lawyer (4)

De acuerdo con la respuesta aceptada (y única) para esta pregunta de desbordamiento de pila ,

Definiendo el constructor con

MyTest() = default;

en su lugar, inicializará en cero el objeto.

Entonces porque hace lo siguiente,

#include <iostream> struct foo { foo() = default; int a; }; struct bar { bar(); int b; }; bar::bar() = default; int main() { foo a{}; bar b{}; std::cout << a.a << '' '' << b.b; }

producir esta salida:

0 32766

¿Ambos constructores definidos son por defecto? ¿Derecha? Y para los tipos de POD, la inicialización predeterminada es cero inicialización.

Y de acuerdo con la respuesta aceptada para esta pregunta ,

  1. Si un miembro POD no se inicializa en el constructor ni a través de la inicialización en clase de C ++ 11, se inicializa por defecto.

  2. La respuesta es la misma independientemente de la pila o el montón.

  3. En C ++ 98 (y no después), se especificó que nuevo int () realiza una inicialización cero.

A pesar de que traté de envolver a mi (aunque sea pequeño ) los constructores predeterminados y la inicialización predeterminada , no pude dar una explicación.


Desde cppreference :

La inicialización de agregados inicializa los agregados. Es una forma de inicialización de lista.

Un agregado es uno de los siguientes tipos:

[recorte]

  • tipo de clase [snip], que tiene

    • [recorte] (hay variaciones para diferentes versiones estándar)

    • no se permiten constructores explícitos, heredados o proporcionados por el usuario (se permiten constructores predeterminados o eliminados explícitamente)

    • [recorte] (hay más reglas, que se aplican a ambas clases)

Dada esta definición, foo es un agregado, mientras que la bar no lo es (tiene un constructor no predeterminado proporcionado por el usuario).

Por lo tanto, para foo , T object {arg1, arg2, ...}; Es sintaxis para la inicialización agregada.

Los efectos de la inicialización agregada son:

  • [recorte] (algunos detalles irrelevantes para este caso)

  • Si el número de cláusulas de inicialización es menor que el número de miembros o la lista de inicializadores está completamente vacía, los miembros restantes se inicializan con valores .

Por lo tanto, aa es el valor inicializado, que para int significa cero inicialización.

Para bar , T object {}; por otro lado es la inicialización del valor (de la instancia de la clase, ¡no la inicialización del valor de los miembros!). Dado que es un tipo de clase con un constructor predeterminado, se llama al constructor predeterminado. El constructor predeterminado que definió por defecto inicializa los miembros (en virtud de no tener inicializadores de miembros), que en el caso de int (con almacenamiento no estático) deja a bb con un valor indeterminado.

Y para los tipos de pod, la inicialización predeterminada es cero inicialización.

No. Esto está mal.

Una palabra sobre su experimento y su conclusión: ver que la salida es cero no significa necesariamente que la variable haya sido cero inicializada. Cero es el número perfectamente posible para un valor de basura.

para eso ejecuté el programa tal vez 5 ~ 6 veces antes de publicar y unas 10 veces ahora, a siempre es cero. b cambia alrededor de un poco.

El hecho de que el valor fuera el mismo varias veces tampoco significa necesariamente que se haya inicializado.

También probé con el set (CMAKE_CXX_STANDARD 14). El resultado fue el mismo.

El hecho de que el resultado sea el mismo con varias opciones de compilador no significa que la variable se haya inicializado. (Aunque en algunos casos, cambiar la versión estándar puede cambiar si se inicializa).

¿Cómo podría sacudir un poco mi RAM de modo que si hubiera cero allí, ahora debería haber algo más?

No hay forma garantizada en C ++ de hacer que el valor del valor no inicializado aparezca como distinto de cero.

La única manera de saber que una variable está inicializada es comparar el programa con las reglas del lenguaje y verificar que las reglas dicen que está inicializada. En este caso aa está efectivamente inicializada.


El tema aquí es bastante sutil. Pensarías que

bar::bar() = default;

le daría un constructor predeterminado generado por el compilador, y lo hace, pero ahora se considera proporcionado por el usuario. [dcl.fct.def.default]/5 estados:

Las funciones predeterminadas explícitamente y las funciones declaradas implícitamente se denominan colectivamente funciones predeterminadas, y la implementación proporcionará definiciones implícitas para ellas ([class.ctor] [class.dtor], [class.copy.ctor], [class.copy.assign ]), lo que podría significar definirlos como eliminados. Una función es provista por el usuario si es declarada por el usuario y no está predeterminada por defecto o eliminada en su primera declaración. Una función provista explícitamente por defecto proporcionada por el usuario (es decir, por defecto explícitamente después de su primera declaración) se define en el punto donde se establece por defecto explícitamente; si tal función se define implícitamente como eliminada, el programa no está bien formado. [Nota: declarar una función como predeterminada después de su primera declaración puede proporcionar una ejecución eficiente y una definición concisa al tiempo que permite una interfaz binaria estable para una base de código en evolución. - nota final]

énfasis mio

Por lo tanto, podemos ver que, dado que no estableció la bar() predeterminada bar() cuando la declaró por primera vez, ahora se considera usuario. Por eso [dcl.init]/8.2

si T es un tipo de clase (posiblemente cv calificado) sin un constructor predeterminado proporcionado o eliminado por el usuario, entonces el objeto se inicializa con cero y se comprueban las restricciones semánticas para la inicialización predeterminada, y si T tiene un constructor predeterminado no trivial , el objeto está inicializado por defecto;

ya no se aplica y no estamos inicializando el valor b sino que lo inicializamos por defecto según [dcl.init]/8.1

si T es un tipo de clase (posiblemente cv calificado) ([clase]) sin un constructor predeterminado ([class.default.ctor]) o un constructor predeterminado que sea proporcionado o eliminado por el usuario, entonces el objeto se inicializa por defecto ;


La diferencia de comportamiento proviene del hecho de que, según [dcl.fct.def.default]/5 , bar::bar es proporcionado por el usuario donde foo::foo no es 1 . Como consecuencia, foo::foo inicializará con valor sus miembros (lo que significa: cero inicialización foo::a ) pero bar::bar permanecerá sin inicializar 2 .

1) [dcl.fct.def.default]/5

Una función es provista por el usuario si es declarada por el usuario y no está predeterminada por defecto o eliminada en su primera declaración.

2)

Desde [dcl.init#6] :

Para inicializar con valor un objeto de tipo T significa:

  • si T es un tipo de clase (posiblemente cv calificado) sin un constructor predeterminado ([class.ctor]) o un constructor predeterminado que es proporcionado o eliminado por el usuario, entonces el objeto está inicializado por defecto;

  • si T es un tipo de clase (posiblemente cv calificado) sin un constructor predeterminado proporcionado o eliminado por el usuario, entonces el objeto se inicializa con cero y se comprueban las restricciones semánticas para la inicialización predeterminada, y si T tiene un constructor predeterminado no trivial , el objeto está inicializado por defecto;

  • ...

Desde [dcl.init.list] :

La inicialización de lista de un objeto o referencia de tipo T se define de la siguiente manera:

  • ...

  • De lo contrario, si la lista de inicializadores no tiene elementos y T es un tipo de clase con un constructor predeterminado, el objeto se inicializa con valores.

De la respuesta de Vittorio Romeo.


Meh, intenté ejecutar el fragmento que proporcionaste como test.cpp , a través de gcc & clang y múltiples niveles de optimización:

steve@steve-pc /tmp> g++ -o test.gcc.O0 test.cpp [ 0s828 | Jan 27 01:16PM ] steve@steve-pc /tmp> g++ -o test.gcc.O2 -O2 test.cpp [ 0s901 | Jan 27 01:16PM ] steve@steve-pc /tmp> g++ -o test.gcc.Os -Os test.cpp [ 0s875 | Jan 27 01:16PM ] steve@steve-pc /tmp> ./test.gcc.O0 0 32764 [ 0s004 | Jan 27 01:16PM ] steve@steve-pc /tmp> ./test.gcc.O2 0 0 [ 0s004 | Jan 27 01:16PM ] steve@steve-pc /tmp> ./test.gcc.Os 0 0 [ 0s003 | Jan 27 01:16PM ] steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp [ 1s089 | Jan 27 01:17PM ] steve@steve-pc /tmp> clang++ -o test.clang.Os -Os test.cpp [ 1s058 | Jan 27 01:17PM ] steve@steve-pc /tmp> clang++ -o test.clang.O2 -O2 test.cpp [ 1s109 | Jan 27 01:17PM ] steve@steve-pc /tmp> ./test.clang.O0 0 274247888 [ 0s004 | Jan 27 01:17PM ] steve@steve-pc /tmp> ./test.clang.Os 0 0 [ 0s004 | Jan 27 01:17PM ] steve@steve-pc /tmp> ./test.clang.O2 0 0 [ 0s004 | Jan 27 01:17PM ] steve@steve-pc /tmp> ./test.clang.O0 0 2127532240 [ 0s002 | Jan 27 01:18PM ] steve@steve-pc /tmp> ./test.clang.O0 0 344211664 [ 0s004 | Jan 27 01:18PM ] steve@steve-pc /tmp> ./test.clang.O0 0 1694408912 [ 0s004 | Jan 27 01:18PM ]

Así que ahí es donde se vuelve interesante, muestra claramente que la construcción de OG de Clang está leyendo números aleatorios, probablemente de espacio de pila.

Rápidamente subí mi IDA para ver qué está pasando:

int __cdecl main(int argc, const char **argv, const char **envp) { __int64 v3; // rax __int64 v4; // rax int result; // eax unsigned int v6; // [rsp+8h] [rbp-18h] unsigned int v7; // [rsp+10h] [rbp-10h] unsigned __int64 v8; // [rsp+18h] [rbp-8h] v8 = __readfsqword(0x28u); // alloca of 0x28 v7 = 0; // this is foo a{} bar::bar((bar *)&v6); // this is bar b{} v3 = std::ostream::operator<<(&std::cout, v7); // this is clearly 0 v4 = std::operator<<<std::char_traits<char>>(v3, 32LL); // 32 = 0x20 = '' '' result = std::ostream::operator<<(v4, v6); // joined as cout << a.a << '' '' << b.b, so this is reading random values!! if ( __readfsqword(0x28u) == v8 ) // stack align check result = 0; return result; }

Ahora, ¿qué hace bar::bar(bar *this) ?

void __fastcall bar::bar(bar *this) { ; }

Hmm, nada. Tuvimos que recurrir al uso del montaje:

.text:00000000000011D0 ; __int64 __fastcall bar::bar(bar *__hidden this) .text:00000000000011D0 public _ZN3barC2Ev .text:00000000000011D0 _ZN3barC2Ev proc near ; CODE XREF: main+20↓p .text:00000000000011D0 .text:00000000000011D0 var_8 = qword ptr -8 .text:00000000000011D0 .text:00000000000011D0 ; __unwind { .text:00000000000011D0 55 push rbp .text:00000000000011D1 48 89 E5 mov rbp, rsp .text:00000000000011D4 48 89 7D F8 mov [rbp+var_8], rdi .text:00000000000011D8 5D pop rbp .text:00000000000011D9 C3 retn .text:00000000000011D9 ; } // starts at 11D0 .text:00000000000011D9 _ZN3barC2Ev endp

Así que sí, es solo que, nada, lo que el constructor básicamente hace es this = this . Pero sabemos que en realidad está cargando direcciones de pila aleatorias no inicializadas e imprimiéndolas.

¿Qué pasa si proporcionamos explícitamente valores para las dos estructuras?

#include <iostream> struct foo { foo() = default; int a; }; struct bar { bar(); int b; }; bar::bar() = default; int main() { foo a{0}; bar b{0}; std::cout << a.a << '' '' << b.b; }

Golpea hasta el clang, oopsie:

steve@steve-pc /tmp> clang++ -o test.clang.O0 test.cpp test.cpp:17:9: error: no matching constructor for initialization of ''bar'' bar b{0}; ^~~~ test.cpp:8:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from ''int'' to ''const bar'' for 1st argument struct bar { ^ test.cpp:8:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion from ''int'' to ''bar'' for 1st argument struct bar { ^ test.cpp:13:6: note: candidate constructor not viable: requires 0 arguments, but 1 was provided bar::bar() = default; ^ 1 error generated. [ 0s930 | Jan 27 01:35PM ]

Destino similar con g ++ también:

steve@steve-pc /tmp> g++ test.cpp test.cpp: In function ‘int main()’: test.cpp:17:12: error: no matching function for call to ‘bar::bar(<brace-enclosed initializer list>)’ bar b{0}; ^ test.cpp:8:8: note: candidate: ‘bar::bar()’ struct bar { ^~~ test.cpp:8:8: note: candidate expects 0 arguments, 1 provided test.cpp:8:8: note: candidate: ‘constexpr bar::bar(const bar&)’ test.cpp:8:8: note: no known conversion for argument 1 from ‘int’ to ‘const bar&’ test.cpp:8:8: note: candidate: ‘constexpr bar::bar(bar&&)’ test.cpp:8:8: note: no known conversion for argument 1 from ‘int’ to ‘bar&&’ [ 0s718 | Jan 27 01:35PM ]

Entonces, esto significa que es efectivamente una bar b(0) inicialización directa bar b(0) , no una inicialización agregada.

Esto probablemente se deba a que si no proporciona una implementación de constructor explícita, esto podría ser un símbolo externo, por ejemplo:

bar::bar() { this.b = 1337; // whoa }

El compilador no es lo suficientemente inteligente como para deducir esto como una llamada en línea o sin operaciones en una etapa no optimizada.