O GCC 6 possui um novo recurso de otimizador : ele assume que this
nem sempre é nulo e otimiza com base nisso.
Agora, a propagação do intervalo de valores assume que o ponteiro this das funções de membro do C ++ é não nulo. Isso elimina verificações comuns de ponteiro nulo, mas também quebra algumas bases de código não conformes (como Qt-5, Chromium, KDevelop) . Como uma solução temporária, pode-se usar -fno-delete-null-pointer-checks. Código incorreto pode ser identificado usando -fsanitize = undefined.
O documento de mudança claramente chama isso de perigoso, pois quebra uma quantidade surpreendente de código usado com freqüência.
Por que essa nova suposição quebraria o código C ++ prático? Existem padrões específicos em que programadores descuidados ou desinformados dependem desse comportamento indefinido específico? Não consigo imaginar alguém escrevendo, if (this == NULL)
porque isso é muito natural.
fonte
this
elas são passadas como um parâmetro implícito, então elas começam a usá-lo como se fosse um parâmetro explícito. Não é. Quando você desreferencia um nulo disso, está invocando o UB como se tivesse desferenciado qualquer outro ponteiro nulo. É só isso. Se você deseja transmitir nullptrs, use um parâmetro explícito, DUH . Não será mais lento, nem mais desajeitado, e o código que possui essa API é profundo nos internos de qualquer maneira, portanto, tem escopo muito limitado. Fim da história, eu acho.Respostas:
Eu acho que a pergunta que precisa ser respondida por que pessoas bem-intencionadas escreveriam os cheques em primeiro lugar.
O caso mais comum é provavelmente se você tem uma classe que faz parte de uma chamada recursiva que ocorre naturalmente.
Se você tinha:
em C, você pode escrever:
Em C ++, é bom tornar isso uma função membro:
Nos primeiros dias do C ++ (antes da padronização), enfatizou-se que as funções-membro eram açúcar sintático para uma função em que o
this
parâmetro está implícito. O código foi escrito em C ++, convertido em equivalente C e compilado. Havia até exemplos explícitos de que compararthis
com nulo era significativo e o compilador Cfront original também se aproveitou disso. Portanto, vindo de um fundo C, a escolha óbvia para a verificação é:Nota: Bjarne Stroustrup até menciona que as regras para
this
foram alteradas ao longo dos anos aquiE isso funcionou em muitos compiladores por muitos anos. Quando a padronização aconteceu, isso mudou. E, mais recentemente, os compiladores começaram a tirar vantagem da chamada de uma função membro em
this
que onullptr
comportamento indefinido de being é, o que significa que essa condição é semprefalse
, e o compilador é livre para omitir isso.Isso significa que, para fazer qualquer travessia dessa árvore, você precisa:
Faça todas as verificações antes de ligar
traverse_in_order
Isso significa também verificar em TODOS os sites de chamadas se você pode ter uma raiz nula.
Não use uma função de membro
Isso significa que você está escrevendo o código antigo do estilo C (talvez como um método estático) e chamando-o explicitamente como um parâmetro. por exemplo. você voltou a escrever
Node::traverse_in_order(node);
e nãonode->traverse_in_order();
no site de chamadas.Acredito que a maneira mais fácil / correta de corrigir esse exemplo específico de uma forma que seja compatível com os padrões é realmente usar um nó sentinela em vez de a
nullptr
.Nenhuma das duas primeiras opções parece tão atraente e, embora o código possa se safar, eles escreveram códigos ruins em
this == nullptr
vez de usar uma correção adequada.Suponho que é assim que algumas dessas bases de código evoluíram para ter
this == nullptr
verificações nelas.fonte
1 == 0
ser um comportamento indefinido? É simplesmentefalse
.this == nullptr
idioma é um comportamento indefinido porque você chamou uma função de membro em um objeto nullptr antes disso, que é indefinido. E o compilador é livre para omitir a verificaçãothis
ser nulo. Eu acho que talvez esse seja o benefício de aprender C ++ em uma época em que o SO existe para entrincheirar os perigos do UB no meu cérebro e me dissuadir de fazer hacks bizarros como esse.Isso ocorre porque o código "prático" foi quebrado e envolveu um comportamento indefinido para começar. Não há razão para usar um nulo
this
, a não ser como uma micro-otimização, geralmente muito prematura.É uma prática perigosa, pois o ajuste de ponteiros devido à passagem da hierarquia de classes pode transformar um nulo
this
em um não nulo. Portanto, no mínimo, a classe cujos métodos devem trabalhar com um nulothis
deve ser uma classe final sem classe base: não pode derivar de nada e não pode derivar. Estamos partindo rapidamente da prática para a terra feia .Em termos práticos, o código não precisa ser feio:
Se a árvore estiver vazia, também conhecida como nula
Node* root
, você não deve chamar nenhum método não estático nela. Período. É perfeitamente bom ter um código de árvore do tipo C que leva um ponteiro de instância por um parâmetro explícito.O argumento aqui parece se resumir à necessidade de escrever métodos não estáticos em objetos que poderiam ser chamados a partir de um ponteiro de instância nula. Não existe essa necessidade. A maneira C-com-objetos de escrever esse código ainda é muito melhor no mundo C ++, porque pode ser, no mínimo, do tipo seguro. Basicamente, o nulo
this
é uma micro-otimização, com um campo de uso tão restrito, que não é perfeitamente adequado para IMHO. Nenhuma API pública deve depender de um valor nulothis
.fonte
this
verificações são escolhidas por vários analisadores de código estático, portanto, não é como se alguém tivesse que procurá-los manualmente. O patch seria provavelmente algumas centenas de linhas de mudanças triviais.this
desreferência nula é uma falha instantânea. Esses problemas serão descobertos rapidamente, mesmo que ninguém se preocupe em executar um analisador estático sobre o código. O C / C ++ segue o mantra "pague apenas pelos recursos que você usa". Se você deseja verificações, deve ser explícito sobre elas e isso significa não fazê-lasthis
, quando for tarde demais, pois o compilador assume quethis
não é nulo. Caso contrário, teria que verificarthis
e, para 99,9999% do código existente, essas verificações são uma perda de tempo.O documento não chama de perigoso. Também não afirma que quebra uma quantidade surpreendente de código . Ele simplesmente aponta algumas bases de código populares que afirma serem conhecidas por confiar nesse comportamento indefinido e que seriam interrompidas devido à alteração, a menos que a opção de solução alternativa seja usada.
Se o código c ++ prático se basear em um comportamento indefinido, as alterações nesse comportamento indefinido poderão quebrá-lo. É por isso que o UB deve ser evitado, mesmo quando um programa que depende dele parece funcionar como planejado.
Não sei se o anti- padrão é amplamente difundido , mas um programador desinformado pode pensar que ele pode corrigir o problema de travar o programa fazendo:
Quando o bug real está desreferenciando um ponteiro nulo em outro lugar.
Tenho certeza de que, se o programador não tiver informações suficientes, ele poderá criar padrões (anti) mais avançados que dependem desse UB.
Eu posso.
fonte
if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere();
Como um bom registro fácil de ler de uma sequência de eventos que um depurador não pode lhe contar com facilidade. Divirta-se depurando isso agora sem gastar horas colocando verificações em todos os lugares quando houver um nulo aleatório repentino em um grande conjunto de dados, no código que você não escreveu ... E a regra UB sobre isso foi feita mais tarde, depois que o C ++ foi criado. Costumava ser válido.-fsanitize=null
serve.-fsanitize=null
registrar os erros no cartão SD / MMC nos pinos # 5,6,10,11 usando SPI? Essa não é uma solução universal. Alguns argumentaram que é contrário aos princípios orientados a objetos acessar um objeto nulo, mas algumas linguagens OOP têm um objeto nulo que pode ser operado, portanto não é uma regra universal de OOP. 1/2Alguns dos códigos "práticos" (maneira engraçada de soletrar "buggy") que foram quebrados eram assim:
e esqueceu de explicar o fato de que
p->bar()
algumas vezes retorna um ponteiro nulo, o que significa que a desreferenciação para chamarbaz()
é indefinida.Nem todo o código que foi quebrado continha explicações
if (this == nullptr)
ouif (!p) return;
verificações. Alguns casos eram simplesmente funções que não acessavam nenhuma variável membro e, portanto, pareciam funcionar bem. Por exemplo:Nesse código, quando você chama
func<DummyImpl*>(DummyImpl*)
com um ponteiro nulo, há uma desreferência "conceitual" do ponteiro para chamarp->DummyImpl::valid()
, mas, na verdade, essa função de membro retornafalse
sem acessar*this
. Issoreturn false
pode ser incorporado e, portanto, na prática, o ponteiro não precisa ser acessado. Portanto, com alguns compiladores, parece funcionar bem: não há segfault para remover a referência nula,p->valid()
é falso, então o código chamado_something_else(p)
, que verifica se há ponteiros nulos, e não faz nada. Nenhuma falha ou comportamento inesperado é observado.Com o GCC 6, você ainda recebe a chamada
p->valid()
, mas o compilador deduz agora a expressão quep
deve ser não nula (caso contrário,p->valid()
seria um comportamento indefinido) e anota essas informações. Essas informações inferidas são usadas pelo otimizador para que, se a chamadado_something_else(p)
for inline, aif (p)
verificação agora for considerada redundante, porque o compilador lembra que não é nulo e, portanto, alinha o código para:Isso agora desreferencia um ponteiro nulo e, portanto, o código que parecia funcionar anteriormente para de funcionar.
Neste exemplo, está o erro
func
, que deveria ter verificado primeiro como nulo (ou os chamadores nunca deveriam tê-lo chamado com nulo):Um ponto importante a ser lembrado é que a maioria das otimizações como essa não é o caso do compilador dizendo "ah, o programador testou esse ponteiro contra nulo, vou removê-lo apenas por ser irritante". O que acontece é que várias otimizações comuns, como inlining e propagação da faixa de valores, se combinam para tornar essas verificações redundantes, porque elas vêm após uma verificação anterior ou uma desreferência. Se o compilador souber que um ponteiro não é nulo no ponto A de uma função, e o ponteiro não é alterado antes de um ponto posterior B na mesma função, ele sabe que também é não nulo em B. Quando ocorre o inlining os pontos A e B podem realmente ser trechos de código que estavam originalmente em funções separadas, mas agora são combinados em um trecho de código, e o compilador pode aplicar seu conhecimento de que o ponteiro não é nulo em mais lugares.
fonte
this
?this
com nulo] " ?this
, em seguida, basta usar-fsanitize=undefined
O padrão C ++ é quebrado de maneiras importantes. Infelizmente, em vez de proteger os usuários contra esses problemas, os desenvolvedores do GCC optaram por usar um comportamento indefinido como uma desculpa para implementar otimizações marginais, mesmo quando lhes foi explicado claramente como isso é prejudicial.
Aqui, uma pessoa muito mais inteligente do que eu explica em grandes detalhes. (Ele está falando sobre C, mas a situação é a mesma lá).
Por que isso é prejudicial?
A simples recompilação do código seguro anteriormente em funcionamento com uma versão mais recente do compilador pode introduzir vulnerabilidades de segurança . Embora o novo comportamento possa ser desabilitado com um sinalizador, os makefiles existentes não têm esse sinalizador definido, obviamente. E como nenhum aviso é produzido, não é óbvio para o desenvolvedor que o comportamento anteriormente razoável mudou.
Neste exemplo, o desenvolvedor incluiu uma verificação de excesso de número inteiro, usando
assert
, que encerrará o programa se um comprimento inválido for fornecido. A equipe do GCC removeu a verificação com base em que o excesso de número inteiro é indefinido; portanto, a verificação pode ser removida. Isso resultou em instâncias reais in-the-wild dessa base de código sendo tornadas vulneráveis após a correção do problema.Leia a coisa toda. É o suficiente para fazer você chorar.
OK, mas e esse?
Na época, havia um idioma bastante comum que era algo assim:
Portanto, o idioma é: se
pObj
não for nulo, use o identificador que ele contém, caso contrário, use um identificador padrão. Isso está encapsulado naGetHandle
funçãoO truque é que chamar uma função não virtual não faz realmente uso do
this
ponteiro, portanto, não há violação de acesso.Eu ainda não entendi
Existe muito código que é escrito assim. Se alguém simplesmente recompilar, sem alterar uma linha, toda chamada para
DoThing(NULL)
é um bug fatal - se você tiver sorte.Se você não tiver sorte, as chamadas para erros de falha se tornam vulnerabilidades de execução remota.
Isso pode ocorrer mesmo automaticamente. Você tem um sistema de construção automatizado, certo? Atualizá-lo para o compilador mais recente é inofensivo, certo? Mas agora não é - não se o seu compilador for o GCC.
OK, então diga a eles!
Eles foram informados. Eles estão fazendo isso com pleno conhecimento das consequências.
mas por que?
Quem pode dizer? Possivelmente:
Ou talvez outra coisa. Quem pode dizer?
fonte