GCC 6 tiene una nueva función optimizadora : se supone que this
no siempre es nula y se optimiza en función de eso.
La propagación del rango de valores ahora supone que este puntero de las funciones miembro de C ++ no es nulo. Esto elimina las comprobaciones de puntero nulo comunes, pero también rompe algunas bases de código no conformes (como Qt-5, Chromium, KDevelop) . Como solución temporal, se puede utilizar -fno-delete-null-pointer-check. El código incorrecto se puede identificar usando -fsanitize = undefined.
El documento de cambio claramente lo llama peligroso porque rompe una cantidad sorprendente de código de uso frecuente.
¿Por qué esta nueva suposición rompería el código práctico de C ++? ¿Existen patrones particulares en los que los programadores descuidados o desinformados confían en este comportamiento indefinido en particular? No puedo imaginar a nadie escribiendo if (this == NULL)
porque eso es muy poco natural.
fuente
this
se pasa como un parámetro implícito, por lo que luego comienzan a usarlo como si fuera un parámetro explícito. No es. Cuando desreferencia un nulo esto, está invocando UB como si desreferenciara cualquier otro puntero nulo. Eso es todo lo que hay que hacer. Si desea pasar nullptrs, use un parámetro explícito, DUH . No será más lento, no será más complicado, y el código que tiene esa API es profundo en las partes internas de todos modos, por lo que tiene un alcance muy limitado. Fin de la historia, creo.Respuestas:
Supongo que la pregunta que debe responderse es por qué las personas bien intencionadas escribirían los cheques en primer lugar.
El caso más común es probablemente si tiene una clase que es parte de una llamada recursiva natural.
Si tuvieras:
en C, podrías escribir:
En C ++, es bueno hacer de esto una función miembro:
En los primeros días de C ++ (antes de la estandarización), se enfatizó que las funciones miembro eran azúcar sintáctica para una función donde el
this
parámetro está implícito. El código fue escrito en C ++, convertido a C equivalente y compilado. Incluso hubo ejemplos explícitos de que compararthis
con nulo era significativo y el compilador original de Cfront también se aprovechó de esto. Entonces, viniendo de un fondo C, la opción obvia para la verificación es:Nota: Bjarne Stroustrup incluso menciona que las reglas
this
han cambiado con los años aquíY esto funcionó en muchos compiladores durante muchos años. Cuando ocurrió la estandarización, esto cambió. Y, más recientemente, los compiladores comenzaron a tomar ventaja de llamar a una función miembro donde
this
el sernullptr
es un comportamiento indefinido, lo que significa que esta condición es siemprefalse
, y el compilador es libre de omitirlo.Eso significa que para atravesar este árbol, debe:
Haga todas las verificaciones antes de llamar
traverse_in_order
Esto significa también verificar en CADA sitio de llamadas si podría tener una raíz nula.
No use una función miembro
Esto significa que está escribiendo el antiguo código de estilo C (quizás como un método estático) y lo está llamando explícitamente al objeto como parámetro. p.ej. ha vuelto a escribir en
Node::traverse_in_order(node);
lugar denode->traverse_in_order();
en el sitio de la llamada.Creo que la forma más fácil / ordenada de arreglar este ejemplo en particular de una manera que cumpla con los estándares es usar un nodo centinela en lugar de a
nullptr
.Ninguna de las dos primeras opciones parece tan atractiva, y aunque el código podría salirse con la suya, escribieron un código incorrecto en
this == nullptr
lugar de usar una solución adecuada.Supongo que así es como evolucionaron algunas de estas bases de código para tener
this == nullptr
controles en ellas.fuente
1 == 0
ser un comportamiento indefinido? Es simplementefalse
.this == nullptr
idioma es un comportamiento indefinido porque has llamado una función miembro en un objeto nullptr antes de eso, que no está definido. Y el compilador es libre de omitir el chequethis
ser anulable. Supongo que tal vez este es el beneficio de aprender C ++ en una era en la que existe SO para afianzar los peligros de UB en mi cerebro y disuadirme de hacer hacks extraños como este.Lo hace porque el código "práctico" estaba roto e involucraba un comportamiento indefinido para empezar. No hay ninguna razón para usar un valor nulo
this
, aparte de una microoptimización, generalmente muy prematura.Es una práctica peligrosa, ya que el ajuste de los punteros debido al recorrido de la jerarquía de clases puede convertir un valor nulo
this
en uno no nulo. Entonces, como mínimo, la clase cuyos métodos se supone que funcionan con un valor nulothis
debe ser una clase final sin clase base: no puede derivarse de nada, y no puede derivarse de ella. Nos estamos yendo rápidamente de lo práctico a lo feo .En términos prácticos, el código no tiene que ser feo:
Si el árbol está vacío, es decir, un valor nulo
Node* root
, no se debe llamar a ningún método no estático en él. Período. Está perfectamente bien tener un código de árbol tipo C que tome un puntero de instancia mediante un parámetro explícito.El argumento aquí parece reducirse a la necesidad de escribir métodos no estáticos en objetos que podrían llamarse desde un puntero de instancia nula. No hay tal necesidad. La forma en que C-con-objetos escribe dicho código es aún más agradable en el mundo de C ++, porque al menos puede ser de tipo seguro. Básicamente, el valor nulo
this
es una microoptimización, con un campo de uso tan estrecho, que rechazarlo está perfectamente bien. Ninguna API pública debería depender de un valor nulothis
.fuente
this
controles son recogidos por varios analizadores de código estático, por lo que no es como si alguien tiene a ellos cazar manualmente todo abajo. El parche sería probablemente un par de cientos de líneas de cambios triviales.this
desreferencia nula es un bloqueo instantáneo. Estos problemas se descubrirán muy rápidamente, incluso si a nadie le importa ejecutar un analizador estático sobre el código. C / C ++ sigue el mantra "paga solo por las funciones que usas". Si desea verificaciones, debe ser explícito sobre ellas y eso significa no hacerlothis
, cuando es demasiado tarde, ya que el compilador suponethis
que no es nulo. De lo contrario, tendría que verificarthis
, y para el 99.9999% del código, tales comprobaciones son una pérdida de tiempo.El documento no lo llama peligroso. Tampoco afirma que rompe una cantidad sorprendente de código . Simplemente señala algunas bases de código populares que, según afirma, se sabe que confían en este comportamiento indefinido y se romperían debido al cambio a menos que se use la opción de solución alternativa.
Si el código práctico de c ++ se basa en un comportamiento indefinido, los cambios en ese comportamiento indefinido pueden romperlo. Es por eso que se debe evitar UB, incluso cuando un programa que se basa en él parece funcionar según lo previsto.
No sé si es anti- patrón extendido , pero un programador desinformado podría pensar que puede arreglar su programa de fallar haciendo:
Cuando el error real hace referencia a un puntero nulo en otro lugar.
Estoy seguro de que si el programador no está lo suficientemente informado, podrán crear patrones (anti) más avanzados que dependan de este UB.
Puedo.
fuente
if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere();
Como un buen registro fácil de leer de una secuencia de eventos que un depurador no puede contarte fácilmente. Diviértete depurando esto ahora sin pasar horas colocando cheques en todas partes cuando hay un nulo aleatorio repentino en un gran conjunto de datos, en un código que no has escrito ... Y la regla UB sobre esto se hizo más tarde, después de que se creó C ++. Solía ser válido.-fsanitize=null
está.-fsanitize=null
registrar los errores en la tarjeta SD / MMC en los pines # 5,6,10,11 usando SPI? Esa no es una solución universal. Algunos han argumentado que ir en contra de los principios orientados a objetos para acceder a un objeto nulo, sin embargo, algunos lenguajes OOP tienen un objeto nulo que se puede operar, por lo que no es una regla universal de OOP. 1/2Algunos de los códigos "prácticos" (forma divertida de deletrear "errores") que se rompieron se veían así:
y se olvidó de tener en cuenta el hecho de que a
p->bar()
veces devuelve un puntero nulo, lo que significa que desreferenciarlo para llamarbaz()
no está definido.No todo el código que se rompió contenía explícitos
if (this == nullptr)
oif (!p) return;
verificaciones. Algunos casos eran simplemente funciones que no tenían acceso a ninguna variable miembro y, por lo tanto, parecían funcionar bien. Por ejemplo:En este código, cuando llama
func<DummyImpl*>(DummyImpl*)
con un puntero nulo, existe una desreferencia "conceptual" del puntero a llamarp->DummyImpl::valid()
, pero de hecho esa función miembro simplemente regresafalse
sin acceder*this
. Esoreturn false
puede estar en línea y, en la práctica, no es necesario acceder al puntero. Entonces, con algunos compiladores parece funcionar bien: no hay segfault para desreferenciar nulo,p->valid()
es falso, por lo que el código llamado_something_else(p)
, que comprueba los punteros nulos, y no hace nada. No se observan accidentes ni comportamientos inesperados.Con GCC 6 todavía recibe la llamada
p->valid()
, pero el compilador ahora deduce de esa expresión que nop
debe ser nula (de lo contrariop->valid()
sería un comportamiento indefinido) y toma nota de esa información. El optimizador utiliza esa información inferida, de modo que si la llamada ado_something_else(p)
se inserta, laif (p)
verificación ahora se considera redundante, porque el compilador recuerda que no es nula, y así alinea el código para:Esto ahora realmente hace referencia a un puntero nulo, por lo que el código que antes parecía funcionar deja de funcionar.
En este ejemplo, el error está en
func
, que debería haber verificado primero nulo (o los llamadores nunca deberían haberlo llamado con nulo):Un punto importante para recordar es que la mayoría de las optimizaciones como esta no son el caso del compilador que dice "ah, el programador probó este puntero contra nulo, lo eliminaré solo para ser molesto". Lo que sucede es que varias optimizaciones de rutina como la línea y la propagación del rango de valores se combinan para hacer que esas verificaciones sean redundantes, ya que vienen después de una verificación anterior, o una desreferencia. Si el compilador sabe que un puntero no es nulo en el punto A en una función, y el puntero no se cambia antes de un punto B posterior en la misma función, entonces sabe que también es no nulo en B. Cuando ocurre la alineación los puntos A y B en realidad podrían ser piezas de código que originalmente estaban en funciones separadas, pero ahora se combinan en una sola pieza de código, y el compilador puede aplicar su conocimiento de que el puntero no es nulo en más lugares.
fuente
this
?this
con nulo] " ?this
, simplemente use-fsanitize=undefined
El estándar C ++ se rompe de manera importante. Desafortunadamente, en lugar de proteger a los usuarios de estos problemas, los desarrolladores de GCC han optado por usar un comportamiento indefinido como una excusa para implementar optimizaciones marginales, incluso cuando se les ha explicado claramente lo dañino que es.
Aquí una persona mucho más inteligente que la que explico con gran detalle. (Está hablando de C pero la situación es la misma allí).
¿Por qué es dañino?
Simplemente recompilar el código seguro que funcionaba anteriormente con una versión más nueva del compilador puede introducir vulnerabilidades de seguridad . Si bien el nuevo comportamiento se puede deshabilitar con una marca, los archivos MAKE existentes no tienen esa marca establecida, obviamente. Y como no se produce ninguna advertencia, no es obvio para el desarrollador que el comportamiento previamente razonable haya cambiado.
En este ejemplo, el desarrollador ha incluido una comprobación de desbordamiento de enteros, que utilizará
assert
, que terminará el programa si se proporciona una longitud no válida. El equipo de GCC eliminó la verificación sobre la base de que el desbordamiento de enteros no está definido, por lo tanto, la verificación se puede eliminar. Esto resultó en instancias reales de esta base de código que se volvió vulnerable después de que se solucionó el problema.Lee todo el asunto. Es suficiente para hacerte llorar.
OK, pero ¿qué hay de este?
Hace mucho tiempo, había un idioma bastante común que decía algo así:
Entonces, el idioma es: si
pObj
no es nulo, usa el identificador que contiene, de lo contrario, utiliza un identificador predeterminado. Esto está encapsulado en laGetHandle
función.El truco es que llamar a una función no virtual en realidad no hace ningún uso del
this
puntero, por lo que no hay violación de acceso.Todavía no lo entiendo
Existe mucho código que está escrito así. Si alguien simplemente lo vuelve a compilar, sin cambiar una línea, cada llamada a
DoThing(NULL)
es un error de bloqueo, si tiene suerte.Si no tiene suerte, las llamadas a fallos se convierten en vulnerabilidades de ejecución remota.
Esto puede ocurrir incluso automáticamente. Tienes un sistema de construcción automatizado, ¿verdad? Actualizarlo al último compilador es inofensivo, ¿verdad? Pero ahora no lo es, no si su compilador es GCC.
OK entonces diles!
Se les ha dicho. Lo están haciendo con pleno conocimiento de las consecuencias.
¿pero por qué?
¿Quién puede decir? Quizás:
O tal vez algo más. ¿Quién puede decir?
fuente