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

135

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á a cero el objeto.

Entonces, ¿por qué 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 predeterminados? ¿Correcto? Y para los tipos de POD, la inicialización predeterminada es la inicialización cero.

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ó new int () como que realizaba inicialización cero.

A pesar de tratar de comprender mi (aunque pequeña ) idea de los constructores predeterminados y la inicialización predeterminada , no pude encontrar una explicación.

Dodgers de pato
fuente
3
Curiosamente, incluso recibo una advertencia para b: main.cpp: 18: 34: advertencia: 'b.bar::b' se usa sin inicializar en esta función [-Wuninitialized] coliru.stacked-crooked.com/a/d1b08a4d6fb4ca7e
tkausl
8
barEl constructor es proporcionado por el usuario, mientras que fooel constructor es el predeterminado.
Jarod42
2
@PeteBecker, entiendo eso. ¿Cómo podría sacudir un poco mi RAM para que si hubiera cero allí, ahora debería ser otra cosa? ;) ps. Ejecuté el programa una docena de veces. No es un gran programa. Puede ejecutarlo y probarlo en su sistema. aes cero bno es. Parece que ase inicializa.
Duck Dodgers el
2
@JoeyMallone Con respecto a "cómo es proporcionado por el usuario": No hay garantía de que la definición de bar::bar()sea ​​visible en main()- podría definirse en una unidad de compilación separada y hacer algo muy no trivial mientras main()solo esté visible la declaración. Creo que estará de acuerdo en que este comportamiento no debería cambiar dependiendo de si coloca bar::bar()la definición en una unidad de compilación separada o no (incluso si toda la situación no es intuitiva).
Max Langhof
2
@balki ¿O int a = 0;quieres ser realmente explícito?
NathanOliver

Respuestas:

109

El problema 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 proporcionada por el usuario si es declarada por el usuario y no está explícitamente predeterminada o eliminada en su primera declaración.Una función explícitamente predeterminada por el usuario (es decir, explícitamente predeterminada después de su primera declaración) se define en el punto donde está explícitamente predeterminada; si dicha función se define implícitamente como eliminada, el programa está mal 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 a una base de código en evolución. - nota final]

énfasis mío

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

si T es un tipo de clase (posiblemente calificado por cv) sin un constructor predeterminado proporcionado o eliminado por el usuario, entonces el objeto se inicializa a 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, bsino que lo inicializamos por defecto según [dcl.init] /8.1

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

NathanOliver
fuente
52
Quiero decir (*_*)... Si incluso usar las construcciones básicas del lenguaje, necesito leer la letra pequeña del borrador del lenguaje, entonces ¡Aleluya! Pero probablemente parece ser lo que dices.
Duck Dodgers el
12
@balki Sí, hacer bar::bar() = defaultfuera de línea es lo mismo que hacerlo en bar::bar(){}línea.
NathanOliver
15
@JoeyMallone Sí, C ++ puede ser bastante complicado. No estoy seguro de cuál es la razón de esto.
NathanOliver
3
Si hay una declaración previa, una definición posterior con la palabra clave predeterminada NO pondrá a cero los miembros. ¿Correcto? Esto es correcto. Es lo que está pasando aquí.
NathanOliver
66
La razón está ahí en su presupuesto: el objetivo de un valor predeterminado fuera de línea es "proporcionar una ejecución eficiente y una definición concisa al tiempo que permite una interfaz binaria estable a una base de código en evolución", en otras palabras, le permite cambiar a un cuerpo escrito por el usuario más tarde si es necesario sin romper ABI. Tenga en cuenta que la definición fuera de línea no está implícitamente en línea y, por lo tanto, solo puede aparecer en una TU de forma predeterminada; otra TU que vea solo la definición de clase no tiene forma de saber si está explícitamente definida como predeterminada.
TC
25

La diferencia en el comportamiento proviene del hecho de que, según [dcl.fct.def.default]/5, bar::bares proporcionado por el usuario donde foo::foono es 1 . Como consecuencia de ello, foo::foose valorará a inicializar sus miembros (es decir: de cero a inicializar foo::a ), pero bar::barse quedará sin inicializar 2 .


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

Una función es proporcionada por el usuario si es declarada por el usuario y no está explícitamente predeterminada o eliminada en su primera declaración.

2)

De [dcl.init # 6] :

Para inicializar un objeto de tipo T significa:

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

  • si T es un tipo de clase (posiblemente calificado por cv) sin un constructor predeterminado proporcionado o eliminado por el usuario, entonces el objeto se inicializa a 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;

  • ...

De [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 tiene un valor inicializado.

De la respuesta de Vittorio Romeo

YSC
fuente
10

De 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 [recorte], que tiene

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

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

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

Dada esta definición, fooes un agregado, mientras barque no lo es (tiene un constructor no predeterminado proporcionado por el usuario).

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

Los efectos de la inicialización agregada son:

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

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

Por a.alo tanto, se inicializa el valor, que intsignifica inicialización cero.

Para bar, T object {};por el contrario es el valor de inicialización (de la instancia de clase, no el valor de inicialización de miembros!). Como es un tipo de clase con un constructor predeterminado, se llama al constructor predeterminado. El constructor predeterminado que definió default inicializa los miembros (en virtud de no tener inicializadores de miembros), que en el caso de int(con almacenamiento no estático) deja b.bun valor indeterminado.

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

No. Esto está mal.


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

para eso ejecuté el programa quizás 5 ~ 6 veces antes de publicar y aproximadamente 10 veces ahora, a siempre es cero. b cambia un poco.

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

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

El hecho de que el resultado sea el mismo con múltiples opciones de compilación no significa que la variable se inicialice. (Aunque en algunos casos, cambiar la versión estándar puede cambiar si se inicializa).

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

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

La única forma de saber que una variable se inicializa es comparar el programa con las reglas del lenguaje y verificar que las reglas dicen que se inicializó. En este caso, de a.ahecho, se inicializa.

eerorika
fuente
"El constructor predeterminado que definió default inicializa los miembros (en virtud de no tener inicializadores de miembros), lo que en caso de int lo deja con un valor indeterminado". -> eh! "para los tipos de pod, la inicialización predeterminada es la inicialización cero". o estoy equivocado?
Duck Dodgers el
2
@JoeyMallone La inicialización predeterminada de los tipos de POD es sin inicialización.
NathanOliver
@NathanOliver, entonces estoy aún más confundido. Entonces, ¿cómo aes que se inicializa? Estaba pensando aque la inicialización predeterminada y la inicialización predeterminada para un POD miembro es, inicialización cero. Es aentonces sólo por suerte siempre viene cero, no importa cuántas veces he ejecutar este programa.
Duck Dodgers
@JoeyMallone Then how come a is initialized.Porque es un valor inicializado. I was thinking a is default initializedNo lo es.
Eerorika
3
@JoeyMallone No te preocupes por eso. Podría hacer un libro de inicialización en C ++. Si tiene la oportunidad, CppCon en youtube tiene algunos videos sobre la inicialización, y lo más decepcionante (como señalar lo malo que es) ser youtube.com/watch?v=7DTlWPgX6zs
NathanOliver el
0

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 ]

Entonces, ahí es donde se pone interesante, muestra claramente que la construcción de C0 O está leyendo números aleatorios, presumiblemente acumula espacio.

Rápidamente encendí 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 bien, ¿qué bar::bar(bar *this)hace?

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

Hmm, nada Tuvimos que recurrir al uso de ensamblaje:

.text:00000000000011D0                               ; __int64 __fastcall bar::bar(bar *__hidden this)
.text:00000000000011D0                                               public _ZN3barC2Ev
.text:00000000000011D0                               _ZN3barC2Ev     proc near               ; CODE XREF: main+20p
.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

Entonces sí, es simplemente nada, lo que básicamente hace el constructor this = this. Pero sabemos que en realidad está cargando direcciones de pila no inicializadas al azar e imprimirlo.

¿Qué pasa si proporcionamos valores explícitamente 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 el sonido metálico, 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 ]

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

Esto probablemente se deba a que si no proporciona una implementación explícita del constructor, 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 no operativa / en línea en una etapa no optimizada.

Steve Fan
fuente