Em um novo emprego, tenho sido sinalizado em revisões de código para códigos como este:
PowerManager::PowerManager(IMsgSender* msgSender)
: msgSender_(msgSender) { }
void PowerManager::SignalShutdown()
{
msgSender_->sendMsg("shutdown()");
}
Disseram-me que o último método deveria ser:
void PowerManager::SignalShutdown()
{
if (msgSender_) {
msgSender_->sendMsg("shutdown()");
}
}
ou seja, I deve colocar um NULL
guarda em torno da msgSender_
variável, mesmo que seja um membro de dados privados. É difícil para mim me impedir de usar palavrões para descrever como me sinto sobre esse pedaço de 'sabedoria'. Quando peço uma explicação, recebo uma ladainha de histórias de horror sobre como algum programador júnior, durante um ano, ficou confuso sobre como uma classe deveria funcionar e excluiu acidentalmente um membro que ele não deveria ter (e a definiu NULL
posteriormente) , aparentemente), e as coisas explodiram em campo logo após o lançamento do produto, e "aprendemos da maneira mais difícil, confie em nós" que é melhor NULL
verificar tudo .
Para mim, isso parece uma programação de culto de carga , pura e simples. Alguns colegas bem-intencionados estão sinceramente tentando me ajudar a 'entender' e ver como isso me ajudará a escrever um código mais robusto, mas ... Não posso deixar de sentir que eles não entendem. .
É razoável que um padrão de codificação exija que cada ponteiro não referenciado em uma função seja verificado NULL
primeiro - mesmo membros de dados privados? (Nota: para contextualizar, fabricamos um dispositivo de eletrônicos de consumo, não um sistema de controle de tráfego aéreo ou outro produto 'falha-igual-morre-de-pessoas'.)
EDIT : No exemplo acima, o msgSender_
colaborador não é opcional. Se for o caso NULL
, indica um erro. A única razão pela qual ele é passado para o construtor é que PowerManager
pode ser testado com uma IMsgSender
subclasse simulada .
RESUMO : Houve ótimas respostas para essa pergunta, obrigado a todos. Aceitei o @aaronps principalmente devido à sua brevidade. Parece haver um consenso geral bastante amplo de que:
- A
NULL
proteção obrigatória para cada ponteiro não referenciado é um exagero, mas - Você pode contornar o debate inteiro usando uma referência (se possível) ou um
const
ponteiro, e assert
As instruções são uma alternativa mais esclarecida aosNULL
guardas para verificar se as pré-condições de uma função são atendidas.
fonte
null
e não fazer nada é apenas uma maneira de mover o erro para baixo na execução, tornando muito mais difícil rastrear de volta à fonte.Respostas:
Depende do 'contrato':
Se
PowerManager
DEVE ter um válidoIMsgSender
, nunca verifique se há nulo, deixe-o morrer mais cedo.Se, por outro lado, PODE ter um
IMsgSender
, então você precisa verificar toda vez que usar, tão simples quanto isso.Comentário final sobre a história do programador júnior, o problema é realmente a falta de procedimentos de teste.
fonte
assert()
função em todos os membros que desreferencia um ponteiro é um compromisso que provavelmente acalmará muitas pessoas, mas até isso parece uma 'paranóia especulativa' para mim. A lista de coisas além da minha capacidade de controlar é infinita e, uma vez que me permito começar a me preocupar com elas, ela se torna uma ladeira realmente escorregadia. Aprendendo a confiar nos meus testes, meus colegas e meu depurador me ajudaram a dormir melhor à noite.Eu sinto que o código deve ler:
Na verdade, é melhor do que proteger o NULL, porque deixa muito claro que a função nunca deve ser chamada se
msgSender_
for NULL. Também garante que você notará se isso acontecer.A "sabedoria" compartilhada de seus colegas ignorará silenciosamente esse erro, com resultados imprevisíveis.
Em geral, os bugs são mais fáceis de corrigir se forem detectados mais perto da causa. Neste exemplo, o protetor NULL proposto levaria a uma mensagem de desligamento não ser definida, o que pode ou não resultar em um erro perceptível. Você teria mais dificuldade em trabalhar de volta para a
SignalShutdown
função do que se todo o aplicativo acabasse de morrer, produzindo um backtrace prático ou um dump principal apontando diretamente paraSignalShutdown()
.É um pouco contra-intuitivo, mas travar assim que algo está errado tende a tornar seu código mais robusto. Isso porque você realmente encontra os problemas e tende a ter causas muito óbvias também.
fonte
Se o msgSender nunca deve ser
null
, você deve colocar anull
verificação apenas no construtor. Isso também é válido para quaisquer outras entradas na classe - coloque a verificação de integridade no ponto de entrada no 'módulo' - classe, função etc.Minha regra geral é executar verificações de integridade entre os limites do módulo - a classe nesse caso. Além disso, uma classe deve ser pequena o suficiente para que seja possível verificar mentalmente rapidamente a integridade da vida útil dos membros da classe - garantindo que erros como exclusões / atribuições nulas sejam evitados. A verificação nula que é executada no código em sua postagem pressupõe que qualquer uso inválido realmente atribua nulo ao ponteiro - o que nem sempre é o caso. Como 'uso inválido' implica inerentemente que quaisquer suposições sobre código regular não se aplicam, não podemos ter certeza de capturar todos os tipos de erros de ponteiro - por exemplo, exclusão, incremento inválido etc.
Além disso - se você tiver certeza de que o argumento nunca pode ser nulo, considere usar referências, dependendo do uso da classe. Caso contrário, considere usar
std::unique_ptr
oustd::shared_ptr
no lugar de um ponteiro bruto.fonte
Não, não é razoável verificar cada referência de ponteiro para o ponteiro
NULL
.As verificações de ponteiro nulo são valiosas nos argumentos da função (incluindo argumentos do construtor) para garantir que as pré-condições sejam atendidas ou para executar as ações apropriadas se um parâmetro opcional não for fornecido, e são úteis para verificar a invariante de uma classe depois que você expõe os elementos internos da classe. Mas se a única razão para um ponteiro se tornar NULL é a existência de um bug, então não faz sentido verificar. Esse bug poderia facilmente ter definido o ponteiro para outro valor inválido.
Se eu estivesse diante de uma situação como a sua, faria duas perguntas:
assert(msgSender_)
vez da verificação nula? Se você acabou de fazer o check-in nulo, pode ter evitado uma falha, mas pode ter criado uma situação pior porque o software continua com a premissa de que uma operação ocorreu enquanto na realidade essa operação foi ignorada. Isso pode levar a outras partes do software se tornarem instáveis.fonte
Este exemplo parece ser mais sobre a vida útil do objeto do que se um parâmetro de entrada é ou não nulo. Como você menciona que o
PowerManager
deve sempre ter um válidoIMsgSender
, passar o argumento pelo ponteiro (permitindo a possibilidade de um ponteiro nulo) me parece uma falha de design ††.Em situações como essa, eu preferiria alterar a interface para que os requisitos do chamador sejam aplicados pelo idioma:
Reescrevê-lo dessa maneira diz que
PowerManager
precisa manter uma referência paraIMsgSender
toda a sua vida. Isso, por sua vez, também estabelece um requisito implícito queIMsgSender
deve durar mais do quePowerManager
e nega a necessidade de qualquer verificação ou afirmação de ponteiro nuloPowerManager
.Você também pode escrever a mesma coisa usando um ponteiro inteligente (via boost ou c ++ 11), para explicitamente forçar
IMsgSender
a viver mais do quePowerManager
:Esse método é preferível se for possível
IMsgSender
garantir que a vida útil não seja mais longa quePowerManager
a (isto é,x = new IMsgSender(); p = new PowerManager(*x);
).† Com relação aos ponteiros: a verificação nula desenfreada dificulta a leitura do código e não melhora a estabilidade (melhora a aparência da estabilidade, o que é muito pior).
Em algum lugar, alguém conseguiu um endereço para guardar a memória
IMsgSender
. É responsabilidade dessa função garantir que a alocação tenha sido bem-sucedida (verificando os valores de retorno da biblioteca ou lidando adequadamente comstd::bad_alloc
exceções), para não repassar indicadores inválidos.Como o
PowerManager
não possuiIMsgSender
(apenas o empresta por um tempo), não é responsável por alocar ou destruir essa memória. Essa é outra razão pela qual prefiro a referência.†† Como você é novo neste trabalho, espero que esteja invadindo o código existente. Então, por falha de design, quero dizer que a falha está no código com o qual você está trabalhando. Portanto, as pessoas que estão sinalizando seu código porque não está verificando ponteiros nulos estão realmente se sinalizando para escrever código que requer ponteiros :)
fonte
Como exceções, as condições de proteção são úteis apenas se você souber o que fazer para se recuperar do erro ou se desejar fornecer uma mensagem de exceção mais significativa.
Engolir um erro (seja uma exceção ou uma verificação de guarda), é apenas a coisa certa a fazer quando o erro não importa. O lugar mais comum para eu ver erros sendo engolidos é no código de log de erros - você não deseja travar um aplicativo porque não conseguiu registrar uma mensagem de status.
Sempre que uma função é chamada e não é um comportamento opcional, ela deve falhar alto, não silenciosamente.
Edit: ao pensar em sua história de programador júnior, parece que o que aconteceu foi que um membro privado foi definido como nulo quando isso nunca deveria ser permitido. Eles tiveram um problema com a escrita inadequada e estão tentando corrigi-la validando após a leitura. Isso é ao contrário. Quando você o identifica, o erro já ocorreu. A solução executável de revisão de código / compilador para isso não é condições de guarda, mas getters e setters ou membros const.
fonte
Como outros observaram, isso depende de poder ou não
msgSender
ser legitimamenteNULL
. O seguinte pressupõe que nunca deve ser NULL.A "correção" proposta pelas outras pessoas da sua equipe viola o princípio " Dead Programs Tell No Lies ". Os erros são realmente difíceis de encontrar como estão. Um método que muda silenciosamente seu comportamento com base em um problema anterior, não apenas torna difícil encontrar o primeiro bug, mas também adiciona um segundo bug próprio.
O junior causou estragos ao não procurar um nulo. E se esse pedaço de código causar estragos ao continuar sendo executado em um estado indefinido (o dispositivo está ativado, mas o programa "pensa" que está desligado)? Talvez outra parte do programa faça algo que só é seguro quando o dispositivo está desligado.
Qualquer uma dessas abordagens evitará falhas silenciosas:
Use as afirmações sugeridas por esta resposta , mas verifique se elas estão ativadas no código de produção. É claro que isso poderia causar problemas se outras afirmações fossem escritas com a suposição de que elas estariam fora de produção.
Lance uma exceção se for nula.
fonte
Eu concordo em capturar o nulo no construtor. Além disso, se o membro for declarado no cabeçalho como:
O ponteiro não poderá ser alterado após a inicialização; portanto, se estiver bom na construção, ficará bom para a vida útil do objeto que o contém. (O objeto apontado para irá não ser const.)
fonte
NULL
.const
ponteiro, não há necessidadeassert()
de ficar de fora do construtor, portanto, essa resposta parece um pouco melhor que a de Kristof Provost (que também foi excelente, mas abordou a questão de maneira indireta também.) Espero que outros votem nisso, pois realmente não faz sentidoassert
em todos os métodos quando você pode apenas fazer o ponteiroconst
.Isso é absolutamente perigoso!
Eu trabalhei com um desenvolvedor sênior em uma base de código C com os "padrões" mais ruins que insistiram na mesma coisa, para verificar cegamente todos os indicadores quanto a nulos. O desenvolvedor acabaria fazendo coisas assim:
Certa vez, tentei remover essa verificação da pré-condição uma vez em uma função e substituí-la por uma
assert
para ver o que aconteceria.Para meu horror, encontrei milhares de linhas de código na base de código que passavam nulos para essa função, mas onde os desenvolvedores, provavelmente confusos, contornaram e adicionaram mais código até que as coisas funcionassem.
Para meu horror ainda maior, descobri que esse problema era predominante em todos os tipos de lugares na base de código, procurando nulos. A base de código havia crescido ao longo de décadas para depender dessas verificações, a fim de poder violar silenciosamente até as condições prévias mais explicitamente documentadas. Ao remover essas verificações mortais a favor de
asserts
, todos os erros humanos lógicos ao longo de décadas na base de código seriam revelados e nós nos afogaríamos neles.Foram necessárias apenas duas linhas de código aparentemente inocentes como este + time e uma equipe para mascarar mil bugs acumulados.
Esses são os tipos de práticas que fazem com que os bugs dependam de outros bugs para que o software funcione. É um cenário de pesadelo . Também faz com que todos os erros lógicos relacionados à violação de tais condições prévias apareçam misteriosamente a um milhão de linhas de código do site em que ocorreu o erro, pois todas essas verificações nulas apenas escondem o erro e ocultam o erro até chegarmos a um lugar que esquecemos esconder o bug.
Simplesmente verificar nulos às cegas em todos os lugares em que um ponteiro nulo viola uma pré-condição é, para mim, insanidade absoluta, a menos que seu software seja tão crítico quanto a falhas de afirmação e falhas de produção que o potencial desse cenário seja preferível.
Então eu diria, absolutamente não. Nem é "seguro". Pode muito bem ser o oposto e mascarar todos os tipos de erros em toda a sua base de código que, ao longo dos anos, pode levar aos cenários mais terríveis.
assert
é o caminho a percorrer aqui. Violações de pré-condições não devem passar despercebidas, caso contrário, a lei de Murphy pode facilmente entrar em ação.fonte
assert
como um mecanismo de documentação para estabelecer quais são as pré-condições e não apenas como uma maneira de forçar um abortamento. Mas também gosto da natureza da falha de afirmação, que mostra exatamente qual linha de código causou uma falha e a condição de afirmação falhou ("documentar a falha"). Pode ser útil se acabarmos usando uma compilação de depuração fora de um depurador e formos pegos desprevenidos.Objective-C , por exemplo, trata cada chamada de método em um
nil
objeto como um não-op que é avaliado como um valor zero-ish. Existem algumas vantagens nessa decisão de design no Objective-C, pelos motivos sugeridos na sua pergunta. O conceito teórico de proteger todas as chamadas de método com nulo tem algum mérito se for bem divulgado e aplicado de forma consistente.Dito isto, o código vive em um ecossistema, não em um vácuo. O comportamento de proteção nula seria não-idiomático e surpreendente em C ++ e, portanto, deve ser considerado prejudicial. Em resumo, ninguém escreve código C ++ dessa maneira, então não faça isso! Como um contra-exemplo, no entanto, observe que chamar
free()
oudelete
em umNULL
em C e C ++ é garantido como não operacional.No seu exemplo, provavelmente valeria a pena colocar uma afirmação no construtor que
msgSender
não seja nula. Se o construtor chamado um método onmsgSender
imediatamente, então há tal afirmação seria necessário, uma vez que deixaria de funcionar direito lá de qualquer maneira. No entanto, como ele é meramente armazenadomsgSender
para uso futuro, não seria óbvio olhar para um rastreamento de pilha deSignalShutdown()
como o valor veio a serNULL
, portanto, uma asserção no construtor tornaria a depuração significativamente mais fácil.Melhor ainda, o construtor deve aceitar uma
const IMsgSender&
referência, que não pode serNULL
.fonte
O motivo pelo qual você é solicitado a evitar desreferências nulas é garantir que seu código seja robusto. Exemplos de programadores juniores há muito tempo são apenas exemplos. Qualquer pessoa pode quebrar o código por acidente e causar uma desreferência nula - especialmente para globais e globais. Em C e C ++, é ainda mais possível acidentalmente, com o recurso de gerenciamento direto de memória. Você pode se surpreender, mas esse tipo de coisa acontece com muita frequência. Mesmo por desenvolvedores muito experientes, experientes e muito experientes.
Você não precisa anular a verificação de tudo, mas precisa se proteger contra desreferências que tenham uma probabilidade decente de serem nulas. Isso geralmente ocorre quando eles são alocados, usados e desreferenciados em diferentes funções. É possível que uma das outras funções seja modificada e quebre sua função. Também é possível que uma das outras funções seja chamada fora de ordem (como se você tiver um desalocador que possa ser chamado separadamente do destruidor).
Prefiro a abordagem que seus colegas de trabalho estão dizendo em combinação com o uso de assert. Falha no ambiente de teste, por isso é mais óbvio que há um problema para corrigir e falhar normalmente na produção.
Você também deve usar uma ferramenta robusta de correção de código, como cobertura ou fortificação. E você deve abordar todos os avisos do compilador.
Edit: como outros já mencionaram, falhar silenciosamente, como em vários exemplos de código, também é geralmente a coisa errada. Se sua função não puder se recuperar do valor que é nulo, ela retornará um erro (ou lançará uma exceção) ao seu chamador. O chamador é responsável por fixar sua ordem de chamada, recuperar ou retornar um erro (ou lançar uma exceção) ao chamador, e assim por diante. Eventualmente, uma função é capaz de recuperar e prosseguir normalmente, e recupera e falha normalmente (como um banco de dados com falha em uma transação devido a um erro interno de um usuário, mas não sai de forma aguda) ou a função determina que o estado do aplicativo está corrompido e irrecuperável e o aplicativo sai.
fonte
NULL
verificações "rotineiras" é a maneira de fazer isso, mas aprecio ouvir uma opinião de alguém fora do meu local de trabalho que está basicamente dizendo: "Aigh. Não parece muito irracional".O uso de um ponteiro em vez de uma referência me diria que
msgSender
é apenas uma opção e aqui a verificação nula estaria correta. O trecho de código é muito lento para decidir isso. Talvez existam outros elementosPowerManager
valiosos (ou testáveis) ...Ao escolher entre ponteiro e referência, peso bem as duas opções. Se eu precisar usar um ponteiro para um membro (mesmo para membros particulares), tenho que aceitar o
if (x)
procedimento toda vez que derreferenciá-lo.fonte
Depende do que você quer que aconteça.
Você escreveu que está trabalhando em "um dispositivo de eletrônicos de consumo". Se, de alguma forma, um bug for introduzido pela configuração
msgSender_
deNULL
, você desejaSignalShutdown
mas continuando o resto de sua operação, ouDependendo do impacto do sinal de desligamento não ser enviado, a opção 1 pode ser uma opção viável. Se o usuário puder continuar ouvindo sua música, mas a tela ainda mostrar o título da faixa anterior, isso pode ser preferível a uma falha completa do dispositivo.
Obviamente, se você escolher a opção 1, um
assert
(como recomendado por outros) é vital para reduzir a probabilidade de um erro desse tipo passar despercebido durante o desenvolvimento. Aif
proteção nula existe apenas para mitigação de falhas no uso da produção.Pessoalmente, também prefiro a abordagem "travar cedo" para compilações de produção, mas estou desenvolvendo um software comercial que pode ser corrigido e atualizado facilmente no caso de um bug. Para dispositivos eletrônicos de consumo, isso pode não ser tão fácil.
fonte