¿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;
}
c++
initialization
copy-constructor
undefined-behavior
Tomek Czajka
fuente
fuente
Respuestas:
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
union
s, 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::memcpy
siempre 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:
fuente
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 bymemcpy
.En general, copiar datos no inicializados es un comportamiento indefinido porque esos datos pueden estar en un estado de captura. Citando esta página:
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
memcpy
para 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.fuente
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.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:
Si el código descendente no se preocupa por los valores de ningún elemento de
x.dat
oy.dat
cuyos índices no figuran en la listaarr
, el código podría optimizarse para: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á.
fuente
Dado que todos los miembros de la
Data
son de tipos primitivos,data2
obtendrá "copia bit por bit" exacta de todos los miembros dedata
. Entonces el valor dedata2.b
será exactamente el mismo que el valor dedata.b
. Sin embargo, el valor exacto dedata.b
no se puede predecir porque no lo ha inicializado explícitamente. Dependerá de los valores de los bytes en la región de memoria asignada paradata
.fuente
std::memcpy
. Nada de esto impide usarstd::memcpy
ostd::memmove
. Solo evita el uso del constructor de copia implícita.