"Sempre inicializar variáveis" não leva à ocultação de bugs importantes?

35

As diretrizes principais do C ++ têm a regra ES.20: sempre inicialize um objeto .

Evite erros usados ​​antes do conjunto e seu comportamento indefinido associado. Evite problemas com a compreensão de inicialização complexa. Simplifique a refatoração.

Mas essa regra não ajuda a encontrar erros, apenas os oculta.
Vamos supor que um programa tenha um caminho de execução em que use uma variável não inicializada. Isso é um bug. Comportamento indefinido à parte, também significa que algo deu errado, e o programa provavelmente não atende aos requisitos do produto. Quando será implantado na produção, pode haver uma perda de dinheiro, ou pior ainda.

Como podemos rastrear bugs? Nós escrevemos testes. Mas os testes não cobrem 100% dos caminhos de execução e nunca cobrem 100% das entradas do programa. Mais do que isso, mesmo um teste cobre um caminho de execução com defeito - ele ainda pode passar. É um comportamento indefinido, afinal, uma variável não inicializada pode ter um valor válido.

Mas, além de nossos testes, temos os compiladores que podem escrever algo como 0xCDCDCDCD em variáveis ​​não inicializadas. Isso melhora um pouco a taxa de detecção dos testes.
Melhor ainda - existem ferramentas como o Sanitizer de Endereço, que captura todas as leituras de bytes de memória não inicializados.

E, finalmente, existem analisadores estáticos, que podem olhar para o programa e dizer que existe uma leitura antes do conjunto nesse caminho de execução.

Portanto, temos muitas ferramentas poderosas, mas se inicializarmos a variável - os desinfetantes não encontrarão nada .

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Existe outra regra - se a execução do programa encontrar um bug, o programa deverá morrer o mais rápido possível. Não é necessário mantê-lo vivo, basta travar, escrever um despejo de acidente e entregá-lo aos engenheiros para investigação.
A inicialização desnecessária de variáveis ​​faz o oposto - o programa está sendo mantido vivo, quando já haveria uma falha de segmentação.

Abyx
fonte
10
Embora eu pense que essa é uma boa pergunta, não entendo seu exemplo. Se ocorrer um erro de leitura e bytes_readnão for alterado (portanto, mantido zero), por que isso deveria ser um bug? O programa ainda pode continuar de maneira sadia, desde que não espere implicitamente bytes_read!=0posteriormente. Portanto, é bom que os desinfetantes não reclamem. Por outro lado, quando bytes_readnão for inicializado de antemão, o programa não poderá continuar de maneira sã; portanto, não inicializar bytes_readrealmente introduz um bug que não existia anteriormente.
Doc Brown
2
@Abyx: mesmo que seja um terceiro, se ele não lida com um buffer começando com \0ele, é buggy. Se estiver documentado para não lidar com isso, o seu código de chamada é incorreto. Se você corrigir seu código de chamada para verificar bytes_read==0antes de usar, retornará ao ponto em que começou: seu código é incorreto se você não inicializar bytes_read, seguro se o fizer. ( Geralmente, as funções são supostamente para encher os seus out-parâmetros, mesmo em caso de um erro : não é realmente Muitas vezes as saídas ou são deixados sozinhos ou indefinido..)
Mat
11
Existe alguma razão para esse código ignorar o err_tretornado por my_read()? Se houver um bug em qualquer lugar do exemplo, é isso.
Blrfl
11
É fácil: apenas inicialize variáveis ​​se for significativo. Se não for, não faça. No entanto, posso concordar que usar dados "fictícios" para fazer isso é ruim, porque oculta bugs.
Pieter B
11
"Existe outra regra: se a execução do programa encontrar um bug, o programa deverá morrer o mais rápido possível. Não há necessidade de mantê-lo vivo, apenas travar, escrever um despejo de acidente e entregá-lo aos engenheiros para investigação.": Tente isso em um vôo software de controle. Boa sorte na recuperação do despejo de acidente dos destroços do avião.
Giorgio

Respostas:

44

Seu raciocínio está errado em várias contas:

  1. As falhas de segmentação estão longe de ocorrer. O uso de uma variável não inicializada resulta em um comportamento indefinido . As falhas de segmentação são uma das maneiras pelas quais esse comportamento pode se manifestar, mas parece normal.
  2. Os compiladores nunca preenchem a memória não inicializada com um padrão definido (como 0xCD). Isso é algo que alguns depuradores fazem para ajudá-lo a encontrar lugares onde variáveis ​​não inicializadas são usadas. Se você executar esse programa fora de um depurador, a variável conterá lixo completamente aleatório. É igualmente provável que um contador como o bytes_readtenha o valor tal 10como ele tem o valor 0xcdcdcdcd.
  3. Mesmo se você estiver executando em um depurador que define a memória não inicializada para um padrão fixo, eles o fazem apenas na inicialização. Isso significa que esse mecanismo só funciona de maneira confiável para variáveis ​​estáticas (e possivelmente alocadas na pilha). Para variáveis ​​automáticas, que são alocadas na pilha ou vivem apenas em um registro, são grandes as chances de a variável ser armazenada em um local que foi usado anteriormente, portanto, o padrão de memória do avisador já foi substituído.

A idéia por trás da orientação para sempre inicializar variáveis ​​é permitir essas duas situações

  1. A variável contém um valor útil desde o início de sua existência. Se você combinar isso com a orientação de declarar uma variável apenas quando precisar, poderá evitar que futuros programadores de manutenção caiam na armadilha de começar a usar uma variável entre sua declaração e a primeira atribuição, onde a variável existiria, mas não seria inicializada.

  2. A variável contém um valor definido que você pode testar posteriormente, para saber se uma função como my_readatualizou o valor. Sem a inicialização, você não pode dizer se bytes_readrealmente tem um valor válido, porque não pode saber com qual valor ele começou.

Bart van Ingen Schenau
fonte
8
1) trata-se de probabilidades, como 1% vs 99%. 2 e 3) O VC ++ gera esse código de inicialização, também para variáveis ​​locais. 3) variáveis ​​estáticas (globais) são sempre inicializadas com 0.
Abyx
5
@Abyx: 1) Na minha experiência, a probabilidade é de ~ 80% "nenhuma diferença comportamental imediatamente óbvia", 10% "faz a coisa errada", 10% "segfault". Quanto a (2) e (3): o VC ++ faz isso apenas nas versões de depuração. Confiar nisso é uma péssima idéia, pois interrompe seletivamente as versões de lançamento e não aparece em muitos dos seus testes.
Christian Aichinger
8
Eu acho que a "idéia por trás da orientação" é a parte mais importante desta resposta. A orientação não está absolutamente lhe dizendo para seguir todas as declarações de variáveis ​​com = 0;. A intenção do conselho é declarar a variável no ponto em que você terá um valor útil para ela e atribuir imediatamente esse valor. Isso é explicitamente claro nas regras a seguir ES21 e ES22 imediatamente a seguir. Todos esses três devem ser entendidos como trabalhando juntos; não como regras individuais não relacionadas.
GrandOpener
11
@GrandOpener Exatamente. Se não houver um valor significativo para atribuir no ponto em que a variável é declarada, o escopo da variável provavelmente está errado.
Kevin Krumwiede
5
"Compiladores nunca enchem", não deveria ser sempre ?
CodesInChaos
25

Você escreveu "esta regra não ajuda a encontrar erros, apenas os esconde" - bem, o objetivo da regra não é ajudar a encontrar erros, mas evitar los. E quando um bug é evitado, não há nada oculto.

Vamos discutir o problema nos termos do seu exemplo: suponha que a my_readfunção tenha o contrato por escrito para inicializar bytes_readem todas as circunstâncias, mas isso não ocorre em caso de erro; portanto, é uma falha, pelo menos, neste caso. Sua intenção é usar o ambiente de tempo de execução para mostrar esse bug, não inicializando o bytes_readparâmetro primeiro. Contanto que você tenha certeza de que existe um desinfetante de endereços, essa é realmente uma maneira possível de detectar esse bug. Para consertar o bug, é necessário alterar omy_read função internamente.

Mas há um ponto de vista diferente, que é pelo menos igualmente válido: o comportamento defeituoso só surge da combinação de não inicializar de bytes_readantemão e chamar my_readdepois (com a expectativa bytes_readinicializada depois disso). Essa é uma situação que ocorre frequentemente em componentes do mundo real quando a especificação escrita para uma função como my_readnão é 100% clara ou mesmo errada sobre o comportamento em caso de erro. No entanto, desde que bytes_readseja inicializado com zero antes da chamada, o programa se comporta da mesma maneira que se a inicialização fosse feita dentro my_readdela, por isso se comporta corretamente, nesta combinação não há nenhum erro no programa.

Portanto, minha recomendação a seguir é: use a abordagem de não inicialização apenas se

  • você deseja testar se uma função ou bloco de código inicializa um parâmetro específico
  • você tem 100% de certeza de que a função em jogo possui um contrato em que é definitivamente errado não atribuir um valor a esse parâmetro
  • você tem 100% de certeza de que o ambiente pode pegar esse

Essas são condições que você normalmente pode organizar no código de teste , para um ambiente de ferramentas específico.

No código de produção, no entanto, é melhor sempre inicializar previamente essa variável, pois é a abordagem mais defensiva, que evita erros no caso de o contrato estar incompleto ou errado, ou no caso de o desinfetante de endereço ou medidas de segurança semelhantes não serem ativadas. E a regra "crash-early" se aplica, como você escreveu corretamente, se a execução do programa encontrar um bug. Porém, ao inicializar uma variável antecipadamente, significa que não há nada errado, não sendo necessário interromper a execução.

Doc Brown
fonte
4
Era exatamente isso que eu pensava quando li. Não está varrendo as coisas para debaixo do tapete, está varrendo-as para a lata de lixo!
corsiKa
22

Sempre inicialize suas variáveis

A diferença entre as situações que você está considerando é que o caso sem inicialização resulta em um comportamento indefinido , enquanto o caso em que você gastou um tempo para inicializar cria um erro bem definido e determinístico . Não posso enfatizar quão extremamente diferentes esses dois casos são suficientes.

Considere um exemplo hipotético que pode ter acontecido com um funcionário hipotético em um programa de simulações hipotéticas. Essa equipe hipotética estava hipoteticamente tentando fazer uma simulação determinística para demonstrar que o produto que estava vendendo hipoteticamente atendia às necessidades.

Ok, vou parar com a palavra injeções. Eu acho que você entendeu ;-)

Nesta simulação, havia centenas de variáveis ​​não inicializadas. Um desenvolvedor executou o valgrind na simulação e notou que havia vários erros de "ramificação no valor não inicializado". "Hmm, isso parece causar um não-determinismo, dificultando a repetição dos testes quando mais precisamos." O desenvolvedor foi para o gerenciamento, mas o gerenciamento estava em um cronograma muito apertado e não podia poupar recursos para rastrear esse problema. "Acabamos inicializando todas as nossas variáveis ​​antes de usá-las. Temos boas práticas de codificação".

Poucos meses antes da entrega final, quando a simulação está no modo de rotatividade total e toda a equipe está correndo para concluir todas as coisas prometidas pela gerência em um orçamento que, como todo projeto já financiado, era muito pequeno. Alguém notou que não podia testar um recurso essencial porque, por alguma razão, o sim determinístico não estava se comportando deterministicamente para depurar.

A equipe inteira pode ter sido interrompida e passou a maior parte de dois meses vasculhando toda a base de código da simulação, corrigindo erros de valor não inicializados em vez de implementar e testar os recursos. Escusado será dizer que o funcionário pulou o "Eu te disse" e foi direto para ajudar outros desenvolvedores a entender o que são valores não inicializados. Curiosamente, os padrões de codificação foram alterados logo após esse incidente, incentivando os desenvolvedores a sempre inicializar suas variáveis.

E este é o tiro de advertência. Esta é a bala que roçou seu nariz. A questão real é muito, muito, muito mais insidiosa do que você imagina.

Usar um valor não inicializado é "comportamento indefinido" (exceto em alguns casos de canto, como char). O comportamento indefinido (ou UB para abreviar) é tão insana e completamente ruim para você, que você nunca deve acreditar que é melhor que a alternativa. Às vezes, você pode identificar que seu compilador específico define o UB e, em seguida, é seguro usá-lo, mas, caso contrário, o comportamento indefinido é "qualquer comportamento que o compilador sinta". Pode fazer algo que você chamaria de "sã", como se tivesse um valor não especificado. Pode emitir códigos de operação inválidos, potencialmente causando a corrupção do seu programa. Pode disparar um aviso no momento da compilação ou o compilador pode até considerá-lo um erro definitivo.

Ou pode não fazer nada

Meu canário na mina de carvão da UB é um caso de um mecanismo SQL que eu li sobre. Perdoe-me por não vinculá-lo, não encontrei o artigo novamente. Houve um problema de saturação de buffer no mecanismo SQL quando você passou um tamanho de buffer maior para uma função, mas apenas em uma versão específica do Debian. O bug foi devidamente registrado e explorado. A parte engraçada foi: a saturação do buffer foi verificada . Havia código para lidar com a saturação de buffer no lugar. Parecia algo assim:

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

Adicionei mais comentários na minha versão, mas a ideia é a mesma. Se o put + dataLengthenvolver, será menor que o putponteiro (eles tiveram verificações de tempo de compilação para garantir que int não assinado fosse do tamanho de um ponteiro, para os curiosos). Se isso acontecer, sabemos que os algoritmos padrão do buffer de anel podem ficar confusos com esse estouro, então retornamos 0. Ou voltamos?

Como se vê, o excesso de ponteiros é indefinido em C ++. Como a maioria dos compiladores está tratando ponteiros como números inteiros, acabamos com comportamentos típicos de excesso de números inteiros, que são o comportamento que queremos. No entanto, este é um comportamento indefinido, ou seja, o compilador é permitido fazer qualquer coisa que quiser.

No caso de esse erro, Debian aconteceu para optar por usar uma nova versão do gcc que nenhum dos outros sabores principais Linux tinha atualizado para em suas versões de produção. Esta nova versão do gcc tinha um otimizador de código morto mais agressivo. O compilador viu o comportamento indefinido e decidiu que o resultado da ifdeclaração seria "o que torna a otimização do código melhor", que era uma tradução absolutamente legal do UB. Dessa forma, assumiu que, como ptr+dataLengthnunca pode estar abaixo ptrsem um estouro de ponteiro de UB, a ifinstrução nunca seria acionada e otimizaria a verificação de saturação de buffer.

O uso de UB "sã" na verdade fez com que um grande produto SQL tivesse uma exploração de buffer overrun que ele tinha escrito código a evitar!

Nunca confie em comportamento indefinido. Sempre.

Cort Ammon
fonte
Para uma leitura muito divertida sobre o comportamento indefinido, software.intel.com/en-us/blogs/2013/01/06/… é uma publicação incrivelmente bem escrita sobre o quão ruim pode ser. No entanto, esse post específico é sobre operações atômicas, o que é muito confuso para a maioria, por isso evito recomendá-lo como um primer para o UB e como ele pode dar errado.
Cort Ammon
11
Eu gostaria que C tivesse intrínsecas para definir um valor l ou uma matriz deles para valores indeterminados não inicializados e sem interceptação, ou valores não especificados, ou transformar valores desagradáveis ​​para valores menos desagradáveis ​​(sem interferência indeterminada ou não especificada), deixando apenas os valores definidos. Os compiladores poderiam usar essas diretivas para ajudar a otimizações úteis, e os programadores poderiam usá-las para evitar a necessidade de escrever código inútil enquanto bloqueavam as "otimizações" de quebra ao usar coisas como técnicas de matriz esparsa.
Supercat
@supercat Seria um recurso interessante, supondo que você esteja direcionando plataformas onde essa é uma solução válida. Um dos exemplos de problemas conhecidos é a capacidade de criar padrões de memória que não são apenas inválidos para o tipo de memória, mas são impossíveis de serem alcançados por meios comuns. boolé um excelente exemplo em que há problemas óbvios, mas eles aparecem em outro lugar, a menos que você presuma que está trabalhando em uma plataforma muito útil como x86 ou ARM ou MIPS, onde todos esses problemas são resolvidos no momento do código de operação.
Cort Ammon
Considere o caso em que um otimizador pode provar que um valor usado para a switché menor que 8, devido aos tamanhos da aritmética de número inteiro, para que eles possam usar instruções rápidas que presumiram que não havia risco de um valor "grande" chegar. De repente, um Um valor não especificado (que nunca poderia ser construído usando as regras do compilador) aparece, fazendo algo inesperado, e de repente você tem um grande salto do final de uma tabela de salto. Permitir resultados não especificados aqui significa que todas as instruções de comutação no programa precisam ter traps extras para dar suporte a esses casos que podem "nunca ocorrer".
Cort Ammon
Se os intrínsecos fossem padronizados, os compiladores poderiam fazer o que fosse necessário para honrar a semântica; se, por exemplo, alguns caminhos de código configuram uma variável e outros não, e um intrínseco diz "converter para valor não especificado se não inicializado ou indeterminado; caso contrário, deixe em paz", um compilador para plataformas com registros "sem valor" teria que insira o código para inicializar a variável antes de qualquer caminho de código ou em qualquer caminho de código em que a inicialização seria perdida, mas a análise semântica necessária para fazer isso é bastante simples.
Supercat
5

Eu trabalho principalmente em uma linguagem de programação funcional em que você não pode reatribuir variáveis. Sempre. Isso elimina completamente essa classe de erros. Isso pareceu uma grande restrição a princípio, mas obriga a estruturar seu código de forma consistente com a ordem em que você aprende novos dados, o que tende a simplificar seu código e facilitar a manutenção.

Esses hábitos também podem ser transportados para linguagens imperativas. Quase sempre é possível refatorar seu código para evitar a inicialização de uma variável com um valor simulado. É isso que essas diretrizes estão dizendo para você fazer. Eles querem que você coloque algo significativo lá, não algo que apenas satisfaça as ferramentas automatizadas.

Seu exemplo com uma API de estilo C é um pouco mais complicado. Nesses casos, quando eu usar a função, inicializarei para zero para impedir que o compilador se queixe, mas uma vez nos my_readtestes de unidade, inicializarei para outra coisa para garantir que a condição de erro funcione corretamente. Você não precisa testar todas as condições de erro possíveis a cada uso.

Karl Bielefeldt
fonte
5

Não, não esconde bugs. Em vez disso, torna o comportamento determinístico de tal maneira que, se um usuário encontrar um erro, um desenvolvedor poderá reproduzi-lo.


fonte
11
E inicializar com -1 pode ser realmente significativo. Onde "int bytes_read = 0" é ruim, porque na verdade você pode ler 0 bytes, inicializando-o com -1 torna muito claro que nenhuma tentativa de ler bytes foi bem-sucedida e você pode testar isso.
Pieter B
4

TL; DR: Existem duas maneiras de corrigir esse programa, inicializando suas variáveis ​​e orando. Apenas um fornece resultados consistentemente.


Antes que eu possa responder sua pergunta, primeiro preciso explicar o que significa Comportamento indefinido . Na verdade, deixarei que um autor do compilador faça a maior parte do trabalho:

Se você não estiver disposto a ler esses artigos, um TL; DR é:

Comportamento indefinido é um contrato social entre o desenvolvedor e o compilador; o compilador assume com fé cega que seu usuário nunca confiará no comportamento indefinido.

O arquétipo de "Demônios que voam do seu nariz" falhou completamente em transmitir as implicações desse fato, infelizmente. Embora destinado a provar que qualquer coisa poderia acontecer, era tão inacreditável que quase sempre foi ignorado.

A verdade, no entanto, é que o comportamento indefinido afeta a compilação em si, muito antes de você tentar usar o programa (instrumentado ou não, dentro de um depurador ou não) e pode mudar totalmente seu comportamento.

Acho o exemplo na parte 2 acima impressionante:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

é transformado em:

void contains_null_check(int *P) {
  *P = 4;
}

porque é óbvio que Pnão pode ser, 0pois é desreferenciado antes de ser verificado.


Como isso se aplica ao seu exemplo?

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

Bem, você cometeu o erro comum de supor que o comportamento indefinido causaria um erro em tempo de execução. Não pode.

Vamos imaginar que a definição de my_readé:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

e proceda como esperado de um bom compilador com inlining:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

Então, como esperado de um bom compilador, otimizamos ramos inúteis:

  1. Nenhuma variável deve ser usada não inicializada
  2. bytes_readseria usado não inicializado se resultnão fosse0
  3. O desenvolvedor é promissor que resultnunca será 0!

Então resultnunca é 0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Oh, resultnunca é usado:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Ah, podemos adiar a declaração de bytes_read:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

E aqui estamos nós, uma transformação estritamente confirmatória do original, e nenhum depurador interceptará uma variável não inicializada porque não há nenhuma.

Andei por esse caminho, entender o problema quando o comportamento e a montagem esperados não correspondem é realmente divertido.

Matthieu M.
fonte
Às vezes, acho que os compiladores devem obter o programa para excluir os arquivos de origem quando executam um caminho UB. Programadores, então, aprender o que significa UB ao seu usuário final ....
mattnz
1

Vamos dar uma olhada no seu código de exemplo:

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

Este é um bom exemplo. Se anteciparmos um erro como esse, podemos inserir a linha assert(bytes_read > 0);e capturar esse bug em tempo de execução, o que não é possível com uma variável não inicializada.

Mas suponha que não, e encontramos um erro dentro da função use(buffer). Carregamos o programa no depurador, verificamos o backtrace e descobrimos que ele foi chamado a partir desse código. Então, colocamos um ponto de interrupção no topo deste trecho, executamos novamente e reproduzimos o bug. Nós avançamos tentando pegá-lo.

Se não inicializamos bytes_read, ele contém lixo. Não contém necessariamente o mesmo lixo de cada vez. Passamos da linha my_read(buffer, &bytes_read);. Agora, se for um valor diferente do que antes, talvez não possamos reproduzir nosso bug! Pode funcionar na próxima vez, na mesma entrada, por acidente completo. Se for consistentemente zero, obtemos um comportamento consistente.

Verificamos o valor, talvez até em um backtrace na mesma execução. Se for zero, podemos ver que algo está errado; bytes_readnão deve ser zero em caso de sucesso. (Ou, se possível, podemos querer inicializá-lo para -1.) Provavelmente, podemos pegar o bug aqui. Se bytes_readum valor plausível, porém, está errado, nós o identificaríamos de relance?

Isso é especialmente verdadeiro para ponteiros: um ponteiro NULL sempre será óbvio em um depurador, pode ser testado com muita facilidade e deve ocorrer falha no hardware moderno se tentarmos desreferê-lo. Um ponteiro de lixo pode causar erros improdutíveis de corrupção de memória posteriormente, e esses são quase impossíveis de depurar.

Davislor
fonte
1

O OP não depende de comportamento indefinido, ou pelo menos não exatamente. De fato, confiar em um comportamento indefinido é ruim. Ao mesmo tempo, o comportamento de um programa em um caso inesperado também é indefinido, mas um tipo diferente de indefinido. Se você definir uma variável para zero, mas você não tinha a intenção de ter um caminho de execução que os usos que o zero inicial, será o seu comportam programa sanely quando você tem um bug e fazer ter um tal caminho? Você está agora no mato; você não planejou usar esse valor, mas está usando mesmo assim. Talvez seja inofensivo, ou talvez cause uma falha no programa, ou talvez faça com que o programa corrompa dados silenciosamente. Você não sabe.

O que o OP está dizendo é que existem ferramentas que ajudarão você a encontrar esse bug, se você permitir. Se você não inicializar o valor, mas utilizá-lo de qualquer maneira, existem analisadores estáticos e dinâmicos que informarão que você possui um erro. Um analisador estático lhe dirá antes mesmo de começar a testar o programa. Se, por outro lado, você inicializar cegamente o valor, os analisadores não poderão dizer que você não planejou usar esse valor inicial e, portanto, seu bug não será detectado. Se você tiver sorte, é inofensivo ou simplesmente trava o programa; se você tiver azar, corrompe silenciosamente os dados.

O único lugar em que não concordo com o OP é no final, onde ele diz "quando já haveria uma falha de segmentação". De fato, uma variável não inicializada não produzirá com segurança uma falha de segmentação. Em vez disso, eu diria que você deve usar ferramentas de análise estática que não permitirão que você tente executar o programa.

Jordan Brown
fonte
0

Uma resposta para sua pergunta precisa ser dividida nos diferentes tipos de variáveis ​​que aparecem dentro de um programa:


Variáveis ​​locais

Normalmente, a declaração deve estar no local em que a variável obtém seu valor pela primeira vez. Não pré-declare variáveis ​​como no estilo antigo C:

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

Isso remove 99% da necessidade de inicialização, as variáveis ​​têm seu valor final desde o início. As poucas exceções são onde a inicialização depende de alguma condição:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

Acredito que é uma boa ideia escrever esses casos assim:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

I. e. afirme explicitamente que alguma inicialização sensata da sua variável é realizada.


Variáveis-membro

Aqui eu concordo com o que os outros respondentes disseram: Estes devem sempre ser inicializados pelas listas de construtores / inicializadores. Caso contrário, você terá dificuldades para garantir a consistência entre seus membros. E se você tiver um conjunto de membros que parece não precisar de inicialização em todos os casos, refatorar sua classe, adicionando esses membros em uma classe derivada onde eles sempre são necessários.


Buffers

É aqui que discordo das outras respostas. Quando as pessoas se tornam religiosas ao inicializar variáveis, elas frequentemente acabam inicializando buffers como este:

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

Acredito que isso quase sempre é prejudicial: o único efeito dessas inicializações é que elas tornam as ferramentas valgrindimpotentes. Qualquer código que leia mais dos buffers inicializados do que deveria é muito provavelmente um bug. Mas com a inicialização, esse bug não pode ser exposto por valgrind. Portanto, não os use, a menos que você realmente confie na memória sendo preenchida com zeros (e, nesse caso, escreva um comentário dizendo para o que você precisa dos zeros).

Eu também recomendaria fortemente adicionar um destino ao seu sistema de compilação que execute todo o testinguite valgrindou uma ferramenta semelhante para expor erros de uso antes da inicialização e vazamentos de memória. Isso é mais valioso do que todas as pré-inicializações de variáveis. Esse valgrinddestino deve ser executado regularmente, o mais importante antes que qualquer código seja publicado.


Variáveis ​​globais

Você não pode ter variáveis ​​globais que não foram inicializadas (pelo menos em C / C ++ etc.), portanto, verifique se essa inicialização é o que você deseja.

cmaster
fonte
Observe que você pode escrever inicializações condicionais com o operador ternário, por exemplo: Base& b = foo() ? new Derived1 : new Derived2;
Davislor
@Lorehead Isso pode funcionar para os casos simples, mas não para os mais complexos: você não quer fazer isso se tiver três ou mais casos e seus construtores usam três ou mais argumentos, simplesmente para facilitar a leitura. razões. E isso nem leva em consideração qualquer cálculo que possa precisar ser feito, como procurar um argumento para um ramo da inicialização em um loop.
C26 de
Para casos mais complicados, você poderia envolver o código de inicialização em uma função de fábrica: Base &b = base_factory(which);. Isso é mais útil se você precisar chamar o código mais de uma vez ou se permitir tornar o resultado uma constante.
29515 Davislor
@Lorehead Isso é verdade, e certamente o caminho a percorrer se a lógica necessária não for simples. No entanto, acredito que existe uma pequena área cinzenta entre onde a inicialização via ?:é uma PITA e uma função de fábrica ainda é um exagero. Esses casos são poucos e distantes entre si, mas eles existem.
C26 de
-2

Um compilador C, C ++ ou Objective-C decente com as opções corretas do compilador informará em tempo de compilação se uma variável é usada antes que seu valor seja definido. Como nesses idiomas o uso do valor de uma variável não inicializada é um comportamento indefinido, "definir um valor antes de usar" não é uma dica, uma diretriz ou uma boa prática, é um requisito de 100%; caso contrário, seu programa está absolutamente danificado. Em outras linguagens, como Java e Swift, o compilador nunca permitirá que você use uma variável antes de ser inicializada.

Há uma diferença lógica entre "inicializar" e "definir um valor". Se eu quiser encontrar a taxa de conversão entre dólares e euros e escreva "double rate = 0.0;" a variável tem um valor definido, mas não é inicializado. O 0.0 armazenado aqui não tem nada a ver com o resultado correto. Nessa situação, se por causa de um erro você nunca armazenar a taxa de conversão correta, o compilador não terá a chance de informar. Se você acabou de escrever "taxa dupla"; e nunca armazenou uma taxa de conversão significativa, diria o compilador.

Portanto: não inicialize uma variável apenas porque o compilador diz que ela é usada sem ser inicializada. Isso está escondendo um bug. O verdadeiro problema é que você está usando uma variável que não deveria estar usando ou que, em um caminho de código, não definiu um valor. Corrija o problema, não o oculte.

Não inicialize uma variável apenas porque o compilador pode dizer que é usada sem ser inicializada. Mais uma vez, você está escondendo problemas.

Declarar variáveis ​​próximas ao uso. Isso aumenta as chances de você poder inicializá-lo com um valor significativo no ponto da declaração.

Evite reutilizar variáveis. Quando você reutiliza uma variável, ela provavelmente é inicializada com um valor inútil quando você a usa para o segundo objetivo.

Foi comentado que alguns compiladores têm falsos negativos e que a verificação de inicialização é equivalente ao problema de interrupção. Ambos são na prática irrelevantes. Se um compilador, conforme citado, não conseguir encontrar o uso de uma variável não inicializada dez anos após o bug ser relatado, é hora de procurar um compilador alternativo. Java implementa isso duas vezes; uma vez no compilador, uma vez no verificador, sem problemas. A maneira mais fácil de contornar o problema de parada não é exigir que uma variável seja inicializada antes do uso, mas que seja inicializada antes do uso de uma maneira que possa ser verificada por um algoritmo simples e rápido.

gnasher729
fonte
Isso parece superficialmente bom, mas depende muito da precisão dos avisos de valor não inicializado. Conseguir isso perfeitamente correto é equivalente ao Problema de Parada, e os compiladores de produção podem e sofrem falsos negativos (isto é, eles não diagnosticam uma variável não inicializada quando deveriam); veja, por exemplo, o bug 18501 do GCC , que não foi corrigido há mais de dez anos.
Zwol 27/12/15
O que você diz sobre o gcc é apenas dito. O resto é irrelevante.
gnasher729
É triste com o GCC, mas se você não entende por que o resto é relevante, precisa se educar.
Zwol