Copiando estruturas com membros não inicializados

29

É válido copiar uma estrutura em que alguns dos membros não são inicializados?

Suspeito que seja um comportamento indefinido, mas, se for o caso, torna perigoso deixar membros não inicializados em uma estrutura (mesmo que esses membros nunca sejam usados ​​diretamente). Então, eu me pergunto se há algo no padrão que permita isso.

Por exemplo, isso é válido?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
Tomek Czajka
fonte
Lembro-me de ter visto uma pergunta semelhante há um tempo, mas não consigo encontrá-la. Esta questão está relacionada como esta .
1201ProgramAlarm

Respostas:

23

Sim, se o membro não inicializado não for um tipo de caractere estreito não assinado ou std::byte, copiar uma estrutura contendo esse valor indeterminado com o construtor de cópias definido implicitamente é um comportamento tecnicamente indefinido, pois é para copiar uma variável com valor indeterminado do mesmo tipo, porque de [dcl.init] / 12 .

Isso se aplica aqui, porque o construtor de cópias gerado implicitamente é, exceto unions, definido para copiar cada membro individualmente como se fosse pela inicialização direta, consulte [class.copy.ctor] / 4 .

Isso também está sujeito ao problema ativo do CWG 2264 .

Suponho que, na prática, você não terá nenhum problema com isso.

Se você quiser ter 100% de certeza, o uso std::memcpysempre terá um comportamento bem definido se o tipo for trivialmente copiável , mesmo se os membros tiverem um valor indeterminado.


Além desses problemas, você deve sempre inicializar os membros da sua classe adequadamente com um valor especificado na construção de qualquer maneira, supondo que você não exija que a classe tenha um construtor padrão trivial . Você pode fazer isso facilmente usando a sintaxe padrão do inicializador de membros para, por exemplo, inicializar com valor os membros:

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

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
noz
fonte
bem .. esse struct não é um POD (dados antigos simples)? Isso significa que os membros serão inicializados com valores padrão? É uma dúvida
Kevin Kouketsu
Não é a cópia superficial neste caso? o que pode dar errado com isso, a menos que um membro não inicializado seja acessado na estrutura copiada?
TruthSeeker
@KevinKouketsu Adicionei uma condição para o caso em que um tipo trivial / POD é necessário.
walnut
@TruthSeeker O padrão diz que é um comportamento indefinido. A razão pela qual geralmente é um comportamento indefinido para variáveis ​​(não membros) é explicada na resposta por AndreySemashev. Basicamente, é para oferecer suporte a representações de trap com memória não inicializada. Se isso se aplica à construção implícita de cópias de estruturas é a questão do problema vinculado do CWG.
noz
@TruthSeeker O construtor de cópia implícita é definido para copiar cada membro individualmente como se fosse pela inicialização direta. Não está definido para copiar a representação do objeto como se fosse memcpy, mesmo para tipos trivialmente copiáveis. A única exceção são as uniões, para as quais o construtor de cópia implícito copia a representação do objeto como se estivesse por memcpy.
walnut
11

Em geral, copiar dados não inicializados é um comportamento indefinido, pois esses dados podem estar em um estado de interceptação. Citando esta página:

Se uma representação de objeto não representa nenhum valor do tipo de objeto, ela é conhecida como representação de interceptação. Acessar uma representação de interceptação de qualquer maneira que não seja a leitura através de uma expressão lvalue do tipo de caractere é um comportamento indefinido.

NaNs de sinalização são possíveis para tipos de ponto flutuante e, em algumas plataformas, números inteiros podem ter representações de interceptação.

No entanto, para tipos trivialmente copiáveis , é possível usar memcpypara copiar a representação bruta do objeto. Fazer isso é seguro, pois o valor do objeto não é interpretado e, em vez disso, a sequência de bytes brutos da representação do objeto é copiada.

Andrey Semashev
fonte
E os dados dos tipos para os quais todos os padrões de bits representam valores válidos (por exemplo, uma estrutura de 64 bytes contendo um unsigned char[64])? Tratar os bytes de uma estrutura como tendo valores não especificados pode impedir desnecessariamente a otimização, mas exigir que os programadores preencham manualmente a matriz com valores inúteis impediria ainda mais a eficiência.
supercat 10/02
A inicialização de dados não é inútil, ela impede o UB, seja causado por representações de interceptação ou usando dados não inicializados posteriormente. A zeragem de 64 bytes (1 ou 2 linhas de cache) não é tão cara quanto parece. E se você tiver grandes estruturas onde é caro, pense duas vezes antes de copiá-las. E tenho certeza que você precisará inicializá-los de qualquer maneira em algum momento.
Andrey Semashev
Operações de código de máquina que não podem afetar o comportamento de um programa são inúteis. A noção de que qualquer ação caracterizada como UB pela Norma deve ser evitada a todo custo, antes dizendo que [nas palavras do C Standards Committee] UB "identifica áreas de possível extensão de linguagem em conformidade", é relativamente recente. Embora eu não tenha visto uma justificativa publicada para o padrão C ++, ela renuncia expressamente à jurisdição sobre o que os programas C ++ são "autorizados" a fazer, recusando-se a categorizar os programas como conformes ou não, o que significa que permitiria extensões semelhantes.
supercat 10/02
-1

Em alguns casos, como o descrito, o Padrão C ++ permite que os compiladores processem construções da maneira que seus clientes acharem mais útil, sem exigir que o comportamento seja previsível. Em outras palavras, essas construções invocam "comportamento indefinido". Isso não implica, no entanto, que tais construções sejam "proibidas", uma vez que o Padrão C ++ renuncia explicitamente à jurisdição sobre o que programas bem formados são "permitidos". Embora eu não conheça nenhum documento publicado do Rationale para o C ++ Standard, o fato de descrever o Comportamento indefinido da mesma forma que o C89 sugere o significado pretendido é semelhante: "O comportamento indefinido dá ao implementador licença para não detectar certos erros de programa difíceis. diagnosticar.

Existem muitas situações em que a maneira mais eficiente de processar algo envolveria escrever as partes de uma estrutura com a qual o código downstream se importaria, enquanto omitir aquelas que o código downstream não se importaria. Exigir que os programas inicializem todos os membros de uma estrutura, incluindo aqueles com os quais nada se importa, impediria desnecessariamente a eficiência.

Além disso, há algumas situações em que pode ser mais eficiente que dados não inicializados se comportem de maneira não determinística. Por exemplo, 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;
}

se o código downstream não se importar com os valores de quaisquer elementos x.datou y.datcujos índices não foram listados arr, o código poderá ser otimizado 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;
  }
}

Essa melhoria na eficiência não seria possível se os programadores precisassem escrever explicitamente todos os elementos do temp.dat , incluindo aqueles que não se importam com o downstream, antes de copiá-lo.

Por outro lado, existem algumas aplicações em que é importante evitar a possibilidade de vazamento de dados. Nesses aplicativos, pode ser útil ter uma versão do código instrumentada para interceptar qualquer tentativa de copiar o armazenamento não inicializado, sem levar em consideração se o código downstream o analisaria, ou pode ser útil ter uma garantia de implementação que qualquer armazenamento cujo conteúdo pudesse ser vazado seria zerado ou substituído por dados não confidenciais.

Pelo que posso dizer, o Padrão C ++ não tenta dizer que qualquer um desses comportamentos é suficientemente mais útil que o outro para justificar sua exigência. Ironicamente, essa falta de especificação pode ter como objetivo facilitar a otimização, mas se os programadores não puderem explorar nenhum tipo de garantia comportamental fraca, qualquer otimização será negada.

supercat
fonte
-2

Como todos os membros do Datasão de tipos primitivos, data2obterá a "cópia bit a bit" exata de todos os membros do data. Portanto, o valor de data2.bserá exatamente o mesmo que o valor de data.b. No entanto, o valor exato do data.bnão pode ser previsto, porque você não o inicializou explicitamente. Isso dependerá dos valores dos bytes na região de memória alocada para o data.

ivan.ukr
fonte
Você pode apoiar isso com uma referência ao padrão? Os links fornecidos por @walnut implicam que este é um comportamento indefinido. Existe uma exceção para PODs no padrão?
Tomek Czajka 12/02
Embora o seguinte não esteja vinculado ao padrão, ainda assim: pt.cppreference.com/w/cpp/language/… "Os objetos TriviallyCopyable podem ser copiados copiando suas representações de objetos manualmente, por exemplo, com std :: memmove. Todos os tipos de dados compatíveis com o C idioma (tipos POD) são trivialmente copiáveis. "
ivan.ukr
O único "comportamento indefinido" nesse caso é que não podemos prever o valor da variável de membro não inicializada. Mas o código compila e executa com êxito.
ivan.ukr
11
O fragmento que você cita fala sobre o comportamento do memmove, mas não é realmente relevante aqui, porque no meu código estou usando o construtor de cópias, não o memmove. As outras respostas implicam que o uso do construtor de cópia resulta em um comportamento indefinido. Eu acho que você também não entende o termo "comportamento indefinido". Isso significa que o idioma não oferece garantias, por exemplo, o programa pode travar ou corromper dados aleatoriamente ou fazer qualquer coisa. Isso não significa apenas que algum valor é imprevisível, isso seria um comportamento não especificado.
Tomek Czajka
@ ivan.ukr O padrão C ++ especifica que os construtores implícitos de copiar / mover agem em relação aos membros como se fosse pela inicialização direta, veja os links na minha resposta. Portanto, a construção cópia não não fazer "uma 'cópia bit a bit' ". Você está correto apenas para tipos de união, para os quais o construtor de cópia implícito está especificado para copiar a representação do objeto como se fosse um manual std::memcpy. Nada disso impede o uso de std::memcpyou std::memmove. Apenas impede o uso do construtor de cópia implícita.
noz