Copiar estructuras con miembros no inicializados

29

¿Es válido copiar una estructura cuyos miembros no están inicializados?

Sospecho que es un comportamiento indefinido, pero si es así, hace que dejar miembros no inicializados en una estructura (incluso si esos miembros nunca se usan directamente) sea bastante peligroso. Entonces me pregunto si hay algo en el estándar que lo permita.

Por ejemplo, ¿es esto válido?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
Tomek Czajka
fuente
Recuerdo haber visto una pregunta similar hace un tiempo, pero no puedo encontrarla. Esta pregunta está relacionada como esta .
1201ProgramaAlarma

Respuestas:

23

Sí, si el miembro no inicializado no es un tipo de carácter estrecho sin signo o std::byte, copiar una estructura que contiene este valor indeterminado con el constructor de copia implícitamente definido es un comportamiento técnicamente indefinido, como lo es para copiar una variable con valor indeterminado del mismo tipo, porque de [dcl.init] / 12 .

Esto se aplica aquí, porque el constructor de copias generado implícitamente está, a excepción de unions, definido para copiar cada miembro individualmente como por inicialización directa, vea [class.copy.ctor] / 4 .

Esto también es tema del problema activo CWG 2264 .

Sin embargo, supongo que en la práctica no tendrás ningún problema con eso.

Si desea estar 100% seguro, el uso std::memcpysiempre tiene un comportamiento bien definido si el tipo se puede copiar trivialmente , incluso si los miembros tienen un valor indeterminado.


Dejando a un lado estos problemas, siempre debe inicializar los miembros de su clase correctamente con un valor específico en la construcción de todos modos, suponiendo que no requiera que la clase tenga un constructor predeterminado trivial . Puede hacerlo fácilmente usando la sintaxis de inicializador de miembro predeterminada para, por ejemplo, inicializar los miembros:

struct Data {
  int a{}, b{};
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
nuez
fuente
bueno ... esa estructura no es un POD (datos antiguos)? ¿Eso significa que los miembros se inicializarán con valores predeterminados? Es una duda
Kevin Kouketsu
¿No es la copia superficial en este caso? ¿Qué puede salir mal con esto a menos que se acceda al miembro no inicializado en la estructura copiada?
TruthSeeker
@KevinKouketsu He agregado una condición para el caso donde se requiere un tipo trivial / POD.
nogal
@TruthSeeker El estándar dice que es un comportamiento indefinido. La razón por la que generalmente se trata de un comportamiento indefinido para las variables (no miembros) se explica en la respuesta de AndreySemashev. Básicamente es para soportar representaciones de trampas con memoria no inicializada. Si esto está destinado a aplicarse a la construcción de copias implícitas de estructuras es la cuestión del problema del CWG vinculado.
nogal
@TruthSeeker El constructor de copia implícita se define para copiar cada miembro individualmente como si fuera por inicialización directa. No está definido para copiar la representación del objeto como si fuera memcpy, incluso para tipos trivialmente copiables. La única excepción son las uniones, para las cuales el constructor de copia implícita copia la representación del objeto como por by memcpy.
nogal
11

En general, copiar datos no inicializados es un comportamiento indefinido porque esos datos pueden estar en un estado de captura. Citando esta página:

Si una representación de objeto no representa ningún valor del tipo de objeto, se conoce como representación de trampa. Acceder a una representación de trampa de cualquier otra manera que no sea leerla a través de una expresión de valor de tipo de carácter es un comportamiento indefinido.

Los NaN de señalización son posibles para los tipos de punto flotante, y en algunas plataformas los enteros pueden tener representaciones de trampa.

Sin embargo, para los tipos trivialmente copiables , es posible usar memcpypara copiar la representación sin formato del objeto. Hacerlo es seguro ya que el valor del objeto no se interpreta y, en cambio, se copia la secuencia de bytes sin formato de la representación del objeto.

Andrey Semashev
fuente
¿Qué pasa con los datos de tipos para los cuales todos los patrones de bits representan valores válidos (por ejemplo, una estructura de 64 bytes que contiene un unsigned char[64])? Tratar los bytes de una estructura con valores no especificados podría impedir innecesariamente la optimización, pero requerir que los programadores llenen manualmente la matriz con valores inútiles impediría aún más la eficiencia.
supercat
La inicialización de datos no es inútil, evita UB, ya sea causada por representaciones de trampa o por el uso de datos no inicializados más adelante. Poner a cero 64 bytes (1 o 2 líneas de caché) no es tan costoso como parece. Y si tiene estructuras grandes donde es costoso, debe pensarlo dos veces antes de copiarlas. Y estoy bastante seguro de que tendrá que inicializarlos de todos modos en algún momento.
Andrey Semashev
Las operaciones de código de máquina que no pueden afectar el comportamiento de un programa son inútiles. La noción de que cualquier acción caracterizada como UB por el Estándar debe evitarse a toda costa, más bien decir que [en palabras del Comité de Estándares C] UB "identifica áreas de posible extensión de lenguaje conforme", es relativamente reciente. Si bien no he visto una justificación publicada para el Estándar C ++, renuncia expresamente a la jurisdicción sobre lo que los "programas C ++ están" permitidos hacer al negarse a clasificar los programas como conformes o no conformes, lo que significa que permitiría extensiones similares.
supercat
-1

En algunos casos, como el descrito, el Estándar C ++ permite a los compiladores procesar construcciones de la manera que sus clientes encuentren más útil, sin requerir que ese comportamiento sea predecible. En otras palabras, tales construcciones invocan "Comportamiento indefinido". Sin embargo, eso no implica que tales construcciones estén "prohibidas" ya que el Estándar C ++ renuncia explícitamente a la jurisdicción sobre lo que los "programas bien formados" tienen permitido hacer. Si bien no conozco ningún documento de Fundamento publicado para el Estándar C ++, el hecho de que describa el Comportamiento indefinido al igual que C89 sugiere que el significado deseado es similar: "El comportamiento indefinido otorga al implementador la licencia para no detectar ciertos errores del programa que son difíciles diagnosticar.

Hay muchas situaciones en las que la forma más eficiente de procesar algo implicaría escribir las partes de una estructura por las que se preocupará el código descendente, mientras se omiten las que no le importan al código descendente. Exigir que los programas inicialicen a todos los miembros de una estructura, incluidos los que nada les importará, obstaculizaría innecesariamente la eficiencia.

Además, hay algunas situaciones en las que puede ser más eficiente que los datos no inicializados se comporten de manera no determinista. Por ejemplo, dado:

struct q { unsigned char dat[256]; } x,y;

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
    temp.dat[arr[i]] = i;
  x=temp;
  y=temp;
}

Si el código descendente no se preocupa por los valores de ningún elemento de x.dato y.datcuyos índices no figuran en la lista arr, el código podría optimizarse para:

void test(unsigned char *arr, int n)
{
  q temp;
  for (int i=0; i<n; i++)
  {
    int it = arr[i];
    x.dat[index] = i;
    y.dat[index] = i;
  }
}

Esta mejora en la eficiencia no sería posible si los programadores tuvieran que escribir explícitamente todos los elementos temp.dat, incluidos aquellos que no están interesados, antes de copiarlo.

Por otro lado, hay algunas aplicaciones en las que es importante evitar la posibilidad de fuga de datos. En tales aplicaciones, puede ser útil tener una versión del código que esté instrumentada para atrapar cualquier intento de copiar almacenamiento no inicializado sin tener en cuenta si el código posterior lo vería, o podría ser útil tener una garantía de implementación que garantice cualquier almacenamiento cuyos contenidos podrían filtrarse se pondrían a cero o se sobrescribirían con datos no confidenciales.

Por lo que puedo decir, el estándar C ++ no intenta decir que ninguno de estos comportamientos es lo suficientemente más útil que el otro como para justificar su mandato. Irónicamente, esta falta de especificación puede estar destinada a facilitar la optimización, pero si los programadores no pueden explotar ningún tipo de garantías de comportamiento débiles, cualquier optimización se negará.

Super gato
fuente
-2

Dado que todos los miembros de la Datason de tipos primitivos, data2obtendrá "copia bit por bit" exacta de todos los miembros de data. Entonces el valor de data2.bserá exactamente el mismo que el valor de data.b. Sin embargo, el valor exacto de data.bno se puede predecir porque no lo ha inicializado explícitamente. Dependerá de los valores de los bytes en la región de memoria asignada para data.

ivan.ukr
fuente
¿Puedes apoyar esto con una referencia al estándar? Los enlaces proporcionados por @walnut implican que este es un comportamiento indefinido. ¿Existe una excepción para los POD en el estándar?
Tomek Czajka
Aunque lo siguiente no es un enlace al estándar, aún así: en.cppreference.com/w/cpp/language/… "Los objetos trivialmente copiables se pueden copiar copiando sus representaciones de objetos manualmente, por ejemplo, con std :: memmove. Todos los tipos de datos son compatibles con C lenguaje (tipos de POD) son trivialmente copiables ".
ivan.ukr
El único "comportamiento indefinido" en este caso es que no podemos predecir el valor de la variable miembro no inicializada. Pero el código se compila y se ejecuta con éxito.
ivan.ukr
1
El fragmento que cita habla sobre el comportamiento de memmove, pero no es realmente relevante aquí porque en mi código estoy usando el constructor de copia, no memmove. Las otras respuestas implican que el uso del constructor de copia da como resultado un comportamiento indefinido. Creo que también malinterpretas el término "comportamiento indefinido". Significa que el lenguaje no ofrece ninguna garantía, por ejemplo, el programa podría bloquearse o corromper datos aleatoriamente o hacer algo. No solo significa que algún valor es impredecible, sería un comportamiento no especificado.
Tomek Czajka
@ ivan.ukr El estándar C ++ especifica que los constructores de copia / movimiento implícitos actúan en función de los miembros como si por inicialización directa, vean los enlaces en mi respuesta. Por lo tanto, la construcción de la copia no hace una " " copia bit por bit " ". Solo es correcto para los tipos de unión, para los cuales el constructor de copia implícita se especifica para copiar la representación del objeto como si fuera un manual std::memcpy. Nada de esto impide usar std::memcpyo std::memmove. Solo evita el uso del constructor de copia implícita.
nogal