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.
bytes_read
nã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 implicitamentebytes_read!=0
posteriormente. Portanto, é bom que os desinfetantes não reclamem. Por outro lado, quandobytes_read
não for inicializado de antemão, o programa não poderá continuar de maneira sã; portanto, não inicializarbytes_read
realmente introduz um bug que não existia anteriormente.\0
ele, é 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 verificarbytes_read==0
antes de usar, retornará ao ponto em que começou: seu código é incorreto se você não inicializarbytes_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..)err_t
retornado pormy_read()
? Se houver um bug em qualquer lugar do exemplo, é isso.Respostas:
Seu raciocínio está errado em várias contas:
bytes_read
tenha o valor tal10
como ele tem o valor0xcdcdcdcd
.A idéia por trás da orientação para sempre inicializar variáveis é permitir essas duas situações
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.
A variável contém um valor definido que você pode testar posteriormente, para saber se uma função como
my_read
atualizou o valor. Sem a inicialização, você não pode dizer sebytes_read
realmente tem um valor válido, porque não pode saber com qual valor ele começou.fonte
= 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.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_read
função tenha o contrato por escrito para inicializarbytes_read
em 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 obytes_read
parâ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_read
antemão e chamarmy_read
depois (com a expectativabytes_read
inicializada depois disso). Essa é uma situação que ocorre frequentemente em componentes do mundo real quando a especificação escrita para uma função comomy_read
não é 100% clara ou mesmo errada sobre o comportamento em caso de erro. No entanto, desde quebytes_read
seja inicializado com zero antes da chamada, o programa se comporta da mesma maneira que se a inicialização fosse feita dentromy_read
dela, 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
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.
fonte
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:
Adicionei mais comentários na minha versão, mas a ideia é a mesma. Se o
put + dataLength
envolver, será menor que oput
ponteiro (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
if
declaraçã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, comoptr+dataLength
nunca pode estar abaixoptr
sem um estouro de ponteiro de UB, aif
instruçã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.
fonte
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.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".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_read
testes 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.fonte
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
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 é:
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:
é transformado em:
porque é óbvio que
P
não pode ser,0
pois é desreferenciado antes de ser verificado.Como isso se aplica ao seu exemplo?
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
é:e proceda como esperado de um bom compilador com inlining:
Então, como esperado de um bom compilador, otimizamos ramos inúteis:
bytes_read
seria usado não inicializado seresult
não fosse0
result
nunca será0
!Então
result
nunca é0
:Oh,
result
nunca é usado:Ah, podemos adiar a declaração de
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.
fonte
Vamos dar uma olhada no seu código de exemplo:
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 linhamy_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_read
não deve ser zero em caso de sucesso. (Ou, se possível, podemos querer inicializá-lo para -1.) Provavelmente, podemos pegar o bug aqui. Sebytes_read
um 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.
fonte
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.
fonte
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:
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:
Acredito que é uma boa ideia escrever esses casos assim:
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:
Acredito que isso quase sempre é prejudicial: o único efeito dessas inicializações é que elas tornam as ferramentas
valgrind
impotentes. 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 porvalgrind
. 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
valgrind
ou 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. Essevalgrind
destino 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.
fonte
Base& b = foo() ? new Derived1 : new Derived2;
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.?:
é uma PITA e uma função de fábrica ainda é um exagero. Esses casos são poucos e distantes entre si, mas eles existem.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.
fonte