É razoável anular a guarda de cada ponteiro não referenciado?

65

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 NULLguarda 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 NULLposteriormente) , 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 NULLverificar 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 NULLprimeiro - 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 PowerManagerpode ser testado com uma IMsgSendersubclasse 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:

  1. A NULLproteção obrigatória para cada ponteiro não referenciado é um exagero, mas
  2. Você pode contornar o debate inteiro usando uma referência (se possível) ou um constponteiro, e
  3. assertAs instruções são uma alternativa mais esclarecida aos NULLguardas para verificar se as pré-condições de uma função são atendidas.
evadeflow
fonte
14
Não pense por um minuto que os erros são de domínio dos programadores juniores. 95% dos desenvolvedores de todos os níveis de experiência desenvolvem algo de vez em quando. Os 5% restantes estão mentindo por entre os dentes.
Blrfl
56
Se eu vi alguém escrever um código que verifica uma falha nula e silenciosa, eu gostaria de acioná-lo imediatamente.
Winston Ewert
16
As coisas que falham silenciosamente são as piores. Pelo menos quando explode, você sabe onde a explosão ocorre. Verificar nulle 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.
precisa
11
+1, pergunta muito interessante. Além da minha resposta, eu também apontaria que, quando você tem que escrever um código cujo objetivo é acomodar um teste de unidade, o que você está realmente fazendo é negociar um risco aumentado por uma falsa sensação de segurança (mais linhas de código = mais possibilidades de erros). A reformulação do design para usar referências não apenas melhora a legibilidade do código, mas também reduz a quantidade de código (e testes) que você precisa escrever para provar que funciona.
Seth
6
@ Rig, tenho certeza de que existem casos apropriados para uma falha silenciosa. Mas se alguém coloca em silêncio falha em como prática geral (a menos que trabalhar em um reino onde faz sentido), eu realmente não quero estar trabalhando em um projeto que eles estão colocando código.
Winston Ewert

Respostas:

66

Depende do 'contrato':

Se PowerManager DEVE ter um válido IMsgSender, 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.

aaronps
fonte
4
+1 para uma resposta muito sucinta que resume muito bem o que eu sinto: "depende ..." Você também teve a coragem de dizer "nunca verifique se é nulo" ( se nulo for inválido). A colocação de uma 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.
Evadeflow
2
Ao ler o código, essas afirmações podem ser muito úteis para entender como o código deve funcionar. Eu nunca encontrei uma base de código que me fez dizer 'eu gostaria que não houvesse todas essas afirmações por todo o lado', mas já vi muitas coisas onde disse o contrário.
21113 Kristof Provost Novost
11
Pura e simples, esta é a resposta correta para a pergunta.
Matthew Azkimov 02/11/2013
2
Esta é a resposta correta mais próxima, mas não posso concordar com 'nunca verificar nulo', sempre verifique parâmetros inválidos no ponto em que serão atribuídos a uma variável de membro.
James
Obrigado pelos comentários. É melhor deixá-lo travar mais cedo se alguém não ler a especificação e a inicializar com valor nulo quando for necessário ter algo válido lá.
aaronps
77

Eu sinto que o código deve ler:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender)
{
    assert(msgSender);
}

void PowerManager::SignalShutdown()
{
    assert(msgSender_);
    msgSender_->sendMsg("shutdown()");
}

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 SignalShutdownfunção do que se todo o aplicativo acabasse de morrer, produzindo um backtrace prático ou um dump principal apontando diretamente para SignalShutdown().

É 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.

Kristof Provost
fonte
10
A grande diferença entre chamar assert e o exemplo de código do OP é que assert só é habilitado em compilações de depuração (#define NDEBUG) Quando o produto chega às mãos do cliente e a depuração é desabilitada, e uma combinação inesgotável de caminhos de código é executada para sair o ponteiro nulo mesmo que a função em questão seja executada, a declaração não fornece proteção.
Atk 02/11
17
Presumivelmente, você teria testado a compilação antes de realmente enviá-la ao cliente, mas sim, esse é um risco possível. As soluções são fáceis: basta enviar com as afirmações ativadas (essa é a versão que você testou de qualquer maneira) ou substituí-la por sua própria macro, que afirma no modo de depuração e apenas logs, logs + backtraces, saídas, ... no modo de liberação.
21413 Kristof Provost
7
@ James - em 90% dos casos, você também não se recuperará da falha na verificação NULL. Mas, nesse caso, o código falhará silenciosamente e o erro poderá aparecer muito mais tarde - por exemplo, você pensou que salvou no arquivo, mas, na verdade, a verificação nula às vezes falha, deixando você menos inteligente. Você não pode se recuperar de toda situação que não deve acontecer. Você pode verificar se eles não acontecerão, mas acho que você não tem um orçamento, a menos que escreva código para satélite ou usina nuclear da NASA. Você precisa ter uma maneira de dizer "Não tenho idéia do que está acontecendo - o programa não deve estar nesse estado".
Maciej Piechotka
6
@ James Não concordo em jogar. Isso faz parecer que é algo que pode realmente acontecer. Afirmações são para coisas que deveriam ser impossíveis. Para erros reais no código em outras palavras. As exceções são para quando coisas inesperadas, mas possíveis, acontecem. Exceções são para coisas com as quais você pode lidar e se recuperar. Erros de lógica no código do qual você não pode se recuperar e também não deve tentar.
21413 Kristof Provost Novost
2
@ James: Na minha experiência com sistemas embarcados, o software e o hardware são invariavelmente projetados de forma que é extremamente difícil tornar um dispositivo completamente inutilizável. Na grande maioria dos casos, há um watchdog de hardware que força uma redefinição completa do dispositivo se o software parar de funcionar.
Bart van Ingen Schenau
30

Se o msgSender nunca deve ser null, você deve colocar a nullverificaçã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_ptrou std::shared_ptrno lugar de um ponteiro bruto.

Máx.
fonte
11

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:

  • Por que o erro daquele programador júnior não foi detectado antes do lançamento? Por exemplo, em uma revisão de código ou na fase de teste? Trazer para o mercado um dispositivo eletrônico de consumo defeituoso pode ser tão caro (se você levar em consideração a participação de mercado e a boa vontade) quanto liberar um dispositivo defeituoso usado em um aplicativo crítico de segurança; portanto, espero que a empresa leve a sério os testes e outras atividades de controle de qualidade.
  • Se a verificação nula falhar, que manipulação de erro você espera que eu coloque em prática ou eu poderia apenas escrever em 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.
Bart van Ingen Schenau
fonte
9

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 PowerManagerdeve sempre ter um válido IMsgSender, 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:

PowerManager::PowerManager(const IMsgSender& msgSender)
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    msgSender_->sendMsg("shutdown()");
}

Reescrevê-lo dessa maneira diz que PowerManagerprecisa manter uma referência para IMsgSendertoda a sua vida. Isso, por sua vez, também estabelece um requisito implícito que IMsgSenderdeve durar mais do que PowerManagere nega a necessidade de qualquer verificação ou afirmação de ponteiro nulo PowerManager.

Você também pode escrever a mesma coisa usando um ponteiro inteligente (via boost ou c ++ 11), para explicitamente forçar IMsgSendera viver mais do que PowerManager:

PowerManager::PowerManager(std::shared_ptr<IMsgSender> msgSender) 
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    // Here, we own a smart pointer to IMsgSender, so even if the caller
    // destroys the original pointer, we still have a valid copy
    msgSender_->sendMsg("shutdown()");
}

Esse método é preferível se for possível IMsgSendergarantir que a vida útil não seja mais longa que PowerManagera (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 com std::bad_allocexceções), para não repassar indicadores inválidos.

Como o PowerManagernão possui IMsgSender(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 :)

Seth
fonte
11
Na verdade, não há nada de errado em usar ponteiros brutos às vezes. Std :: shared_ptr não é uma bala de prata e usá-lo em uma lista de argumentos é uma cura para a engenharia superficial, embora eu perceba que é considerado 'leet' usá-lo sempre que possível. Use java se você se sentir assim.
James
2
Eu não disse que ponteiros crus são ruins! Eles definitivamente têm seu lugar. No entanto, este é C + + , referências (e ponteiros compartilhados, agora) são parte da linguagem por uma razão. Use-os, eles farão com que você tenha sucesso.
Seth
Eu gosto da idéia do ponteiro inteligente, mas não é uma opção nesse show em particular. +1 por recomendar o uso de referências (como o @Max e alguns outros o fizeram). Às vezes não é possível injetar uma referência, porém, devido a problemas de dependência de galinha e do ovo, ou seja, se um objeto que de outra forma seria um candidato para injeção precisa ter um ponteiro (ou de referência) ao seu titular injetado lo . Às vezes, isso pode indicar um design fortemente acoplado; mas geralmente é aceitável, como no relacionamento entre um objeto e seu iterador, onde nunca faz sentido usá-lo sem o primeiro.
Evadeflow 03/11/2013
8

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.

jmoreno
fonte
7

Como outros observaram, isso depende de poder ou não msgSenderser legitimamente NULL . O seguinte pressupõe que nunca deve ser NULL.

void PowerManager::SignalShutdown()
{
    if (!msgSender_)
    {
       throw SignalException("Shut down failed because message sender is not set.");
    }

    msgSender_->sendMsg("shutdown()");
}

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:

  1. 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.

  2. Lance uma exceção se for nula.

cutucar
fonte
5

Eu concordo em capturar o nulo no construtor. Além disso, se o membro for declarado no cabeçalho como:

IMsgSender* const msgSender_;

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.)

Grimm The Opiner
fonte
Esse é um ótimo conselho, obrigado! Tão bom, na verdade, que sinto muito por não poder votar mais. Não é uma resposta direta à pergunta declarada, mas é uma ótima maneira de eliminar qualquer possibilidade de o ponteiro se tornar "acidentalmente" NULL.
Evadeflow
@evadeflow Eu pensei que "eu concordo com todos os outros" respondeu ao impulso principal da pergunta. Cheers embora! ;-)
Grimm O Opiner
Seria um pouco indelicado da minha parte mudar a resposta aceita neste momento, dada a maneira como a pergunta foi formulada. Mas realmente acho que esse conselho é excelente e lamento não ter pensado nisso. Com um constponteiro, não há necessidade assert()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 sentido assertem todos os métodos quando você pode apenas fazer o ponteiro const.
Evadeflow
@evadeflow As pessoas só visitam o Questins para o representante, agora já foi respondido, ninguém vai vê-lo novamente. ;-) Só apareci porque era uma pergunta sobre ponteiros e estou de olho na sangrenta brigada "Use ponteiros inteligentes para tudo sempre".
Grimm The Opiner #
3

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:

// Pre: vertex should never be null.
void transform_vertex(Vertex* vertex, ...)
{
    // Inserted by my "wise" co-worker.
    if (!vertex)
        return;
    ...
}

Certa vez, tentei remover essa verificação da pré-condição uma vez em uma função e substituí-la por uma assertpara 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.

É razoável que um padrão de codificação exija que cada ponteiro não referenciado em uma função seja verificado primeiro para NULL - mesmo para membros de dados privados?

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
11
"afirmar" é o caminho a percorrer - mas lembre-se de que em qualquer sistema moderno, cada acesso à memória por meio de um ponteiro possui uma declaração de ponteiro nulo incorporada ao hardware. Assim, em "assert (p! = NULL); return * p;" mesmo a assertiva é inútil.
precisa saber é o seguinte
@ gnasher729 Ah, esse é um argumento muito bom, mas eu costumo ver assertcomo 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.
@ gnasher729 Ou talvez eu tenha entendido mal uma parte disso. Esses sistemas modernos mostram qual linha de código fonte na qual a afirmação falhou? Eu geralmente imagino que eles perderam as informações nesse ponto e podem apenas mostrar violação de segfault / acesso, mas estou meio longe de ser "moderno" - ainda teimosamente no Windows 7 durante a maior parte do meu desenvolvimento.
Por exemplo, no MacOS X e iOS, se o aplicativo travar devido a um acesso nulo ao ponteiro durante o desenvolvimento, o depurador informará a linha exata do código onde isso acontece. Há outro aspecto estranho: o compilador tentará dar avisos se você fizer algo suspeito. Se você passar um ponteiro para uma função e acessá-la, o compilador não emitirá um aviso de que o ponteiro pode ser NULL, porque isso acontece com tanta frequência que você se afogaria em avisos. No entanto, se você fizer uma comparação if (p == null) ... então o compilador assume que há uma boa chance de que p é NULL,
gnasher729
porque por que você o testaria de outra forma, portanto, ele emitirá um aviso a partir de então se você o usar sem testar. Se você usar sua própria macro de asserção, precisará ser um pouco inteligente para escrevê-la de uma maneira que "my_assert (p! = NULL," seja um ponteiro nulo, estúpido! "); * P = 1;" não lhe dá um aviso.
precisa saber é o seguinte
2

Objective-C , por exemplo, trata cada chamada de método em um nilobjeto 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()ou deleteem um NULLem C e C ++ é garantido como não operacional.


No seu exemplo, provavelmente valeria a pena colocar uma afirmação no construtor que msgSendernão seja nula. Se o construtor chamado um método on msgSenderimediatamente, 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 armazenado msgSender para uso futuro, não seria óbvio olhar para um rastreamento de pilha de SignalShutdown()como o valor veio a ser NULL, 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 ser NULL.

200_success
fonte
1

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.

atk
fonte
+1 por ter a coragem de apoiar uma posição (aparentemente) impopular. Eu ficaria decepcionado se ninguém defendesse meus colegas sobre isso (são pessoas muito inteligentes que lançam um produto de qualidade há muitos anos). Também gosto da ideia de que os aplicativos devem "travar no ambiente de teste ... e falhar normalmente na produção". Não estou convencido de que exigir NULLverificaçõ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".
Evadeflow 02/11/2013
Fico aborrecido quando vejo pessoas testando NULL após malloc (). Isso me diz que eles não entendem como os sistemas operacionais modernos gerenciam a memória.
21113 Kristof Provost Novost
2
Meu palpite é que o @KristofProvost está falando de sistemas operacionais que usam overcommit, para os quais o malloc sempre é bem-sucedido (mas o sistema operacional pode posteriormente matar o seu processo se não houver memória suficiente disponível). No entanto, esse não é um bom motivo para ignorar as verificações nulas no malloc: a supercomprovação não é universal entre plataformas e, mesmo que esteja ativada, pode haver outros motivos para a falha do malloc (por exemplo, no Linux, um limite de espaço de endereço do processo definido com ulimit )
John Bartholomew
11
O que John disse. Eu estava realmente falando de supercomprometimento. A confirmação excessiva é ativada por padrão no Linux (que é o que paga minhas contas). Não sei, mas suspeito que sim, se é o mesmo no Windows. Na maioria dos casos, você acaba caindo de qualquer maneira, a menos que esteja preparado para escrever muito código que nunca será testado para lidar com erros que quase certamente nunca acontecerão ...
Kristof Provost
2
Deixe-me acrescentar que também nunca vi código (fora do kernel do linux, onde a memória é realmente realista possível) que realmente lidam corretamente com uma condição de falta de memória. Todo o código que eu vi tentar travaria mais tarde de qualquer maneira. Tudo o que foi realizado foi perder tempo, dificultar a compreensão do código e ocultar o problema real. As desreferências nulas são fáceis de depurar (se você tiver um arquivo principal).
21113 Kristof Provost
1

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 elementos PowerManagervaliosos (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.

Lobo
fonte
Como mencionei em outro comentário em algum lugar, há momentos em que a injeção de uma referência não é possível. Um exemplo eu me deparo com um monte é onde o próprio objeto realizada necessita de uma referência ao titular:
evadeflow
@evadeflow: OK, confesso, depende do tamanho da classe e da visibilidade dos membros do ponteiro (que não podem ser feitas referências), se eu verifico antes da re-referência. Nos casos em que a classe é pequena e simples e o ponteiro (s) são privados, eu não ...
Lobo
0

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_de NULL, você deseja

  • o dispositivo para continuar, pulando SignalShutdownmas continuando o resto de sua operação, ou
  • o dispositivo travar, forçando o usuário a reiniciá-lo?

Dependendo 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. A ifproteçã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.

Heinzi
fonte