Desorden de inicialización por defecto, valor y cero

88

Estoy muy confundido sobre el valor, el valor predeterminado y la inicialización cero. y especialmente cuando entran en juego para los diferentes estándares C ++ 03 y C ++ 11 (y C ++ 14 ).

Estoy citando y tratando de extender una respuesta realmente buena Value- / Default- / Zero- Init C ++ 98 y C ++ 03 aquí para hacerlo más general, ya que ayudaría a muchos usuarios si alguien pudiera ayudar a completar el los espacios necesarios para tener una buena visión general de lo que sucede cuando?

La visión completa mediante ejemplos en pocas palabras:

A veces, la memoria devuelta por el nuevo operador se inicializará y, a veces, no dependerá de si el tipo que está creando es un POD (datos antiguos sin formato) o si es una clase que contiene miembros de POD y está usando un constructor predeterminado generado por el compilador.

  • En C ++ 1998 hay 2 tipos de inicialización: inicialización cero y por defecto
  • En C ++ 2003 se agregó un tercer tipo de inicialización, inicialización de valor .
  • En C ++ 2011 / C ++ 2014, solo se agregó la inicialización de lista y las reglas para la inicialización de valor / predeterminado / cero cambiaron un poco.

Asumir:

struct A { int m; };                     
struct B { ~B(); int m; };               
struct C { C() : m(){}; ~C(); int m; };  
struct D { D(){}; int m; };             
struct E { E() = default; int m;}; /** only possible in c++11/14 */  
struct F {F(); int m;};  F::F() = default; /** only possible in c++11/14 */

En un compilador de C ++ 98, debería ocurrir lo siguiente :

  • new A - valor indeterminado ( Aes POD)
  • new A()- inicializar cero
  • new B - construcción predeterminada ( B::mno está inicializada, Bno es POD)
  • new B()- construcción predeterminada ( B::mno está inicializada)
  • new C - construcción predeterminada ( C::mse inicializa a cero, Cno es POD)
  • new C()- construcción predeterminada ( C::mse inicializa a cero)
  • new D - construcción predeterminada ( D::mno está inicializada, Dno es POD)
  • new D()- construcción predeterminada? ( D::mno está inicializado)

En un compilador conforme a C ++ 03, las cosas deberían funcionar así:

  • new A - valor indeterminado ( Aes POD)
  • new A() - value-initialize A, que es una inicialización cero ya que es un POD.
  • new B - default-initializes (deja sin B::minicializar, Bno es POD)
  • new B() - valor-inicializa B que cero todos los campos, ya que su ctor predeterminado es generado por el compilador en lugar de definido por el usuario.
  • new C - default-initializes C, que llama al ctor predeterminado. ( C::mestá inicializado a cero, Cno es POD)
  • new C() - value-initializes C, que llama al ctor predeterminado. ( C::mestá inicializado a cero)
  • new D - construcción predeterminada ( D::mno está inicializada, Dno es POD)
  • new D() - valor-inicializa D? , que llama al ctor predeterminado ( D::mno está inicializado)

Valores en cursiva y? son incertidumbres, por favor ayude a corregir esto :-)

En un compilador conforme a C ++ 11, las cosas deberían funcionar así:

??? (por favor ayúdenme si empiezo aquí, de todos modos saldrá mal)

En un compilador compatible con C ++ 14, las cosas deberían funcionar así: ??? (por favor ayude si empiezo aquí, de todos modos saldrá mal )

  • new A - default-initializes A, compilador gen. ctor, (deja sin A::minicializar) ( Aes POD)

  • new A() - valor-inicializa A, que es cero-inicialización desde 2. punto en [dcl.init] / 8

  • new B - default-initializes B, compilador gen. ctor, (deja sin B::minicializar) ( Bno es POD)

  • new B() - value-initializes,B que inicializa a cero todos los campos, ya que su ctor predeterminado es generado por el compilador en lugar de definido por el usuario.

  • new C - default-initializes C, que llama al ctor predeterminado. ( C::mestá inicializado a cero, Cno es POD)

  • new C() - value-initializes C, que llama al ctor predeterminado. ( C::mestá inicializado a cero)

  • new D - default-initializes D( D::mno está inicializado, Dno es POD)

  • new D() - value-initializes D, que llama al ctor predeterminado ( D::mno está inicializado)

  • new E - default-initializes E, que llama a comp. gen. ctor. ( E::mno está inicializado, E no es POD)

  • new E() - value-initializes E, que se inicializa a cero Edesde el punto 2 en [dcl.init] / 8 )

  • new F - default-initializes F, que llama a comp. gen. ctor. ( F::mno está inicializado, Fno es POD)

  • new F() - value-initializes F, que se inicializa por defecto F desde 1. punto en [dcl.init] / 8 (la Ffunción ctor es proporcionada por el usuario si está declarada por el usuario y no está explícitamente predeterminada o eliminada en su primera declaración. Enlace )

Gabriel
fuente
hay una buena explicación aquí: en.cppreference.com/w/cpp/language/default_constructor
Richard Hodges
1
Por lo que puedo decir, solo hay una diferencia entre C ++ 98 y C ++ 03 en estos ejemplos. El problema parece estar descrito en N1161 (hay revisiones posteriores de ese documento) y CWG DR # 178 . La redacción necesitaba cambiar en C ++ 11 debido a nuevas características y una nueva especificación de POD, y cambió nuevamente en C ++ 14 debido a defectos en la redacción de C ++ 11, pero los efectos en estos casos no se modifican .
dyp
3
Aunque aburrido, struct D { D() {}; int m; };puede valer la pena incluirlo en su lista.
Yakk - Adam Nevraumont

Respuestas:

24

C ++ 14 especifica la inicialización de objetos creados con new[expr.new] / 17 ([expr.new] / 15 en C ++ 11, y la nota no era una nota sino texto normativo en ese entonces):

Una nueva expresión que crea un objeto de tipo Tinicializa ese objeto de la siguiente manera:

  • Si se omite el nuevo inicializador , el objeto se inicializa por defecto (8.5). [ Nota: si no se realiza ninguna inicialización, el objeto tiene un valor indeterminado. - nota final ]
  • De lo contrario, el nuevo inicializador se interpreta de acuerdo con las reglas de inicialización de 8.5 para inicialización directa .

La inicialización predeterminada se define en [dcl.init] / 7 (/ 6 en C ++ 11, y la redacción en sí tiene el mismo efecto):

Para default-inicializar un objeto de tipo Tmedio:

  • si Tes un tipo de clase (posiblemente calificado para cv) (Cláusula 9), Tse llama al constructor predeterminado (12.1) para (y la inicialización está mal formada si Tno tiene un constructor predeterminado o una resolución de sobrecarga (13.3) resulta en una ambigüedad o en una función que está eliminada o inaccesible desde el contexto de la inicialización);
  • si Tes un tipo de matriz, cada elemento se inicializa por defecto ;
  • de lo contrario, no se realiza ninguna inicialización.

Así

  • new Aúnicamente hace Aque se llame al constructor predeterminado s, que no se inicializa m. Valor indeterminado. Debería ser el mismo para new B.
  • new A() se interpreta de acuerdo con [dcl.init] / 11 (/ 10 en C ++ 11):

    Un objeto cuyo inicializador es un conjunto vacío de paréntesis, es decir (), se inicializará con valor.

    Y ahora considere [dcl.init] / 8 (/ 7 en C ++ 11 †):

    Para valor-inicializar un objeto de tipo Tmedio:

    • si Tes un tipo de clase (posiblemente calificado para cv) (Cláusula 9) sin un constructor predeterminado (12.1) o un constructor predeterminado proporcionado por el usuario o eliminado, entonces el objeto se inicializa por defecto;
    • si Tes un tipo de clase (posiblemente calificado por cv) sin un constructor predeterminado proporcionado por el usuario o eliminado, entonces el objeto se inicializa en cero y se verifican las restricciones semánticas para la inicialización predeterminada, y si T tiene un constructor predeterminado no trivial, el objeto está inicializado por defecto;
    • si Tes un tipo de matriz, entonces cada elemento tiene valor inicializado;
    • de lo contrario, el objeto se inicializa a cero.

    Por new A()lo tanto, se inicializará a cero m. Y esto debería ser equivalente a Ay B.

  • new Cy new C()volverá a inicializar el objeto por defecto, ya que se aplica el primer punto de la última cita (¡C tiene un constructor predeterminado proporcionado por el usuario!). Pero, claramente, ahora mse inicializa en el constructor en ambos casos.


† Bueno, este párrafo tiene una redacción ligeramente diferente en C ++ 11, lo que no altera el resultado:

Para valor-inicializar un objeto de tipo Tmedio:

  • si Tes un tipo de clase (posiblemente calificado para cv) (Cláusula 9) con un constructor proporcionado por el usuario (12.1), entonces T se llama al constructor predeterminado para (y la inicialización está mal formada si T no tiene un constructor predeterminado accesible);
  • si Tes un tipo de clase sin unión (posiblemente calificado por cv) sin un constructor proporcionado por el usuario, entonces el objeto se inicializa en cero y, si Tel constructor predeterminado declarado implícitamente no es trivial, se llama a ese constructor.
  • si Tes un tipo de matriz, entonces cada elemento tiene valor inicializado;
  • de lo contrario, el objeto se inicializa a cero.
Columbo
fuente
@Gabriel no realmente.
Columbo
ah, entonces está hablando principalmente de c ++ 14 y las referencias para c ++ 11 se dan entre paréntesis
Gabriel
@Gabriel Correcto. Quiero decir, C ++ 14 es el estándar más reciente, por lo que está en primer plano.
Columbo
1
Lo molesto de intentar rastrear las reglas de inicialización a través de estándares es que muchos de los cambios (¿la mayoría? ¿Todos?) Entre los estándares publicados de C ++ 14 y C ++ 11 ocurrieron a través de DR, y también lo son de facto C ++ 11. . Y luego también hay DR posteriores a C ++ 14 ...
TC
@Columbo Todavía no entiendo por qué struct A { int m; }; struct C { C() : m(){}; int m; };producir resultados diferentes y qué causa que m en A se inicialice en primer lugar. Abrí un hilo dedicado para el experimento que hice y agradeceré su contribución para aclarar el problema. Gracias stackoverflow.com/questions/45290121/…
darkThoughts
12

La siguiente respuesta extiende la respuesta https://stackoverflow.com/a/620402/977038 que serviría como referencia para C ++ 98 y C ++ 03

Citando la respuesta

  1. En C ++ 1998 hay 2 tipos de inicialización: cero y por defecto
  2. En C ++ 2003 se agregó un tercer tipo de inicialización, inicialización de valor.

C ++ 11 (en referencia a n3242)

Inicializadores

8.5 Inicializadores [dcl.init] especifica que un POD variable o no POD se puede inicializar como inicializador de llave o igual, que puede ser una lista de inicialización con llaves o una cláusula de inicialización denominada de forma agregada como llave o igual. inicializador o usando (lista de expresiones) . Antes de C ++ 11, solo se admitía (lista-expresión) o cláusula-inicializador, aunque la cláusula-inicializador era más restringida que la que tenemos en C ++ 11. En C ++ 11, initializer-clause ahora admite la lista de inicialización con llaves además de la expresión de asignacióncomo en C ++ 03. La siguiente gramática resume la nueva cláusula admitida, donde la parte en negrita se agregó recientemente en el estándar C ++ 11.

inicializador: inicializador
    -llave-o-igual
    (lista-expresión) inicializador -
llave-o-igual:
    = cláusula
    -inicializador-lista
-inicial- llave- inicial-cláusula - inicialización:
    expresión-asignación
    -lista
-inicial-llaves-lista
    -inicializador : cláusula-inicializador ... opt
    lista-inicializador, cláusula-inicializador ... opt ** lista-inicial-entre
-llaves:
    {lista-inicializador, opt}
    {}

Inicialización

Como C ++ 03, C ++ 11 todavía admite tres formas de inicialización


Nota

La parte resaltada en negrita se ha agregado en C ++ 11 y la que está tachada se ha eliminado de C ++ 11.

  1. Tipo de inicializador: 8.5.5 [dcl.init] _zero-initialize_

Realizado en los siguientes casos

  • Los objetos con una duración de almacenamiento estática o de subprocesos se inicializan en cero
  • Si hay menos inicializadores que elementos de matriz, cada elemento no inicializado explícitamente se inicializará en cero.
  • Durante la inicialización de valor , si T es un tipo de clase sin unión (posiblemente calificado por cv) sin un constructor proporcionado por el usuario, entonces el objeto se inicializa en cero.

Inicializar a cero un objeto o referencia de tipo T significa:

  • si T es un tipo escalar (3.9), el objeto se establece en el valor 0 (cero), tomado como una expresión constante integral , convertido a T;
  • si T es un tipo de clase sin unión (posiblemente calificado por cv) , cada miembro de datos no estáticos y cada subobjeto de clase base se inicializa en cero se inicializa en cero y el relleno se inicializa en bits cero;
  • si T es un tipo de unión (posiblemente calificado por cv) , el primer miembro de datos con nombre no estático del objeto se inicializa en cero y el relleno se inicializa en bits cero;
  • si T es un tipo de matriz, cada elemento se inicializa en cero;
  • si T es un tipo de referencia, no se realiza ninguna inicialización.

2. Tipo de inicializador: 8.5.6 [dcl.init] _default-initialize_

Realizado en los siguientes casos

  • Si se omite el nuevo inicializador, el objeto se inicializa por defecto; si no se realiza ninguna inicialización, el objeto tiene un valor indeterminado.
  • Si no se especifica ningún inicializador para un objeto, el objeto se inicializa por defecto, excepto para los Objetos con duración de almacenamiento estática o de subprocesos
  • Cuando una clase base o un miembro de datos no estáticos no se menciona en una lista de inicializador de constructor y se llama a ese constructor.

Inicializar por defecto un objeto de tipo T significa:

  • si T es un (posiblemente calificado para cv) tipo de clase no POD (Cláusula 9), se llama al constructor predeterminado para T (y la inicialización está mal formada si T no tiene un constructor predeterminado accesible);
  • si T es un tipo de matriz, cada elemento se inicializa por defecto;
  • de lo contrario, no se realiza ninguna inicialización.

Nota Hasta C ++ 11, solo los tipos de clase que no son POD con duración de almacenamiento automático se consideraban inicializados por defecto cuando no se usaba ningún inicializador.


3. Tipo de inicializador: 8.5.7 [dcl.init] _value-initialize_

  1. Cuando un objeto (temporal sin nombre, variable con nombre, duración de almacenamiento dinámico o miembro de datos no estáticos) cuyo inicializador es un conjunto vacío de paréntesis, es decir, () o llaves {}

Inicializar con valor un objeto de tipo T significa:

  • si T es un tipo de clase (posiblemente calificado para cv) (Cláusula 9) con un constructor proporcionado por el usuario (12.1), entonces se llama al constructor predeterminado para T (y la inicialización está mal formada si T no tiene un constructor predeterminado accesible) ;
  • si T es un tipo de clase sin unión (posiblemente calificado por cv) sin un constructor proporcionado por el usuario, entonces cada miembro de datos no estáticos y componente de clase base de T tiene valor inicializado; entonces el objeto se inicializa en cero y, si el constructor predeterminado declarado implícitamente de T no es trivial, se llama a ese constructor.
  • si T es un tipo de matriz, entonces cada elemento tiene valor inicializado;
  • de lo contrario, el objeto se inicializa a cero.

Así que para resumir

Nota La cita relevante de la norma está resaltada en negrita

  • new A: default-initializes (deja A :: m sin inicializar)
  • new A (): inicializa A cero, ya que el valor inicializado candidato no tiene un constructor predeterminado proporcionado por el usuario o eliminado. si T es un tipo de clase sin unión (posiblemente calificado por cv) sin un constructor proporcionado por el usuario, entonces el objeto se inicializa en cero y, si el constructor predeterminado declarado implícitamente de T no es trivial, se llama a ese constructor.
  • new B: default-initializes (deja B :: m sin inicializar)
  • new B (): valor inicializa B que inicializa a cero todos los campos; si T es un tipo de clase (posiblemente calificado por cv) (Cláusula 9) con un constructor proporcionado por el usuario (12.1), entonces se llama al constructor predeterminado para T
  • new C: default-initializes C, que llama al ctor predeterminado. si T es un tipo de clase (posiblemente calificado para cv) (Cláusula 9), se llama al constructor predeterminado para T , Además, si se omite el nuevo inicializador, el objeto se inicializa por defecto
  • new C (): valor-inicializa C, que llama al ctor predeterminado. si T es un tipo de clase (posiblemente calificado para cv) (Cláusula 9) con un constructor proporcionado por el usuario (12.1), entonces se llama al constructor predeterminado para T. Además, un objeto cuyo inicializador es un conjunto vacío de paréntesis, es decir, (), se inicializará con valor
Abhijit
fuente
0

Puedo confirmar que en C ++ 11, todo lo mencionado en la pregunta en C ++ 14 es correcto, al menos según las implementaciones del compilador.

Para verificar esto, agregué el siguiente código a mi conjunto de pruebas . Probé con -std=c++11 -O3en GCC 7.4.0, GCC 5.4.0, 10.0.1 Clang, y VS 2017, y todos los ensayos siguientes pase.

#include <gtest/gtest.h>
#include <memory>

struct A { int m;                    };
struct B { int m;            ~B(){}; };
struct C { int m; C():m(){}; ~C(){}; };
struct D { int m; D(){};             };
struct E { int m; E() = default;     };
struct F { int m; F();               }; F::F() = default;

// We use this macro to fill stack memory with something else than 0.
// Subsequent calls to EXPECT_NE(a.m, 0) are undefined behavior in theory, but
// pass in practice, and help illustrate that `a.m` is indeed not initialized
// to zero. Note that we initially tried the more aggressive test
// EXPECT_EQ(a.m, 42), but it didn't pass on all compilers (a.m wasn't equal to
// 42, but was still equal to some garbage value, not zero).
//
#define FILL { int m = 42; EXPECT_EQ(m, 42); }

// We use this macro to fill heap memory with something else than 0, before
// doing a placement new at that same exact location. Subsequent calls to
// EXPECT_EQ(a->m, 42) are undefined behavior in theory, but pass in practice,
// and help illustrate that `a->m` is indeed not initialized to zero.
//
#define FILLH(b) std::unique_ptr<int> bp(new int(42)); int* b = bp.get(); EXPECT_EQ(*b, 42)

TEST(TestZero, StackDefaultInitialization)
{
    { FILL; A a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; B a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; C a; EXPECT_EQ(a.m, 0); }
    { FILL; D a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a; EXPECT_NE(a.m, 0); } // UB!
    { FILL; F a; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackValueInitialization)
{
    { FILL; A a = A(); EXPECT_EQ(a.m, 0); }
    { FILL; B a = B(); EXPECT_EQ(a.m, 0); }
    { FILL; C a = C(); EXPECT_EQ(a.m, 0); }
    { FILL; D a = D(); EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a = E(); EXPECT_EQ(a.m, 0); }
    { FILL; F a = F(); EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, StackListInitialization)
{
    { FILL; A a{}; EXPECT_EQ(a.m, 0); }
    { FILL; B a{}; EXPECT_EQ(a.m, 0); }
    { FILL; C a{}; EXPECT_EQ(a.m, 0); }
    { FILL; D a{}; EXPECT_NE(a.m, 0); } // UB!
    { FILL; E a{}; EXPECT_EQ(a.m, 0); }
    { FILL; F a{}; EXPECT_NE(a.m, 0); } // UB!
}

TEST(TestZero, HeapDefaultInitialization)
{
    { FILLH(b); A* a = new (b) A; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); B* a = new (b) B; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); C* a = new (b) C; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); F* a = new (b) F; EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapValueInitialization)
{
    { FILLH(b); A* a = new (b) A(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D(); EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E(); EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F(); EXPECT_EQ(a->m, 42); } // ~UB
}

TEST(TestZero, HeapListInitialization)
{
    { FILLH(b); A* a = new (b) A{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); B* a = new (b) B{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); C* a = new (b) C{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); D* a = new (b) D{}; EXPECT_EQ(a->m, 42); } // ~UB
    { FILLH(b); E* a = new (b) E{}; EXPECT_EQ(a->m, 0);  }
    { FILLH(b); F* a = new (b) F{}; EXPECT_EQ(a->m, 42); } // ~UB
}

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Los lugares donde UB!se menciona son comportamientos indefinidos, y es probable que el comportamiento real dependa de muchos factores ( a.mpodría ser igual a 42, 0 o alguna otra basura). Los lugares donde ~UBse menciona también son comportamientos indefinidos en teoría, pero en la práctica, debido al uso de una ubicación nueva, es muy poco probable que a->msea ​​igual a otra cosa que 42.

Boris Dalstein
fuente