¿Es razonable proteger nulo cada puntero desreferenciado?

65

En un nuevo trabajo, me han marcado en revisiones de código para código como este:

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

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

Me dijeron que el último método debería leer:

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

es decir, que debe poner un NULLguardia alrededor de la msgSender_variables, a pesar de que es un miembro de datos privados. Es difícil para mí contenerme del uso de improperios para describir cómo me siento acerca de esta 'sabiduría'. Cuando solicito una explicación, recibo una letanía de historias de terror sobre cómo un programador junior, algún año, se confundió acerca de cómo se suponía que debía funcionar una clase y eliminó accidentalmente a un miembro que no debería tener (y lo configuró NULLdespués) , aparentemente), y las cosas explotaron en el campo justo después del lanzamiento de un producto, y hemos "aprendido por las malas, confía en nosotros" que es mejor simplemente NULLverificar todo .

Para mí, esto se siente como una programación de culto de carga , simple y llanamente. Algunos colegas bien intencionados están tratando de ayudarme a "entenderlo" y ver cómo esto me ayudará a escribir código más robusto, pero ... no puedo evitar sentir que son ellos los que no lo entienden .

¿Es razonable que un estándar de codificación requiera que cada puntero desreferenciado en una función se verifique NULLprimero, incluso los miembros de datos privados? (Nota: para dar un poco de contexto, creamos un dispositivo de electrónica de consumo, no un sistema de control de tráfico aéreo o algún otro producto de 'falla-igual-muere gente').

EDITAR : en el ejemplo anterior, el msgSender_colaborador no es opcional. Si alguna vez NULL, indica un error. La única razón por la que se pasa al constructor es porque PowerManagerpuede probarse con una IMsgSendersubclase simulada .

RESUMEN : Hubo algunas respuestas realmente buenas a esta pregunta, gracias a todos. Acepté el de @aaronps principalmente debido a su brevedad. Parece haber un acuerdo general bastante amplio de que:

  1. Los NULLguardias obligatorios para cada puntero desreferenciado son excesivos, pero
  2. Puede esquivar todo el debate utilizando una referencia en su lugar (si es posible) o un constpuntero, y
  3. assertLas declaraciones son una alternativa más ilustrada que los NULLguardias para verificar que se cumplen las condiciones previas de una función.
evadeflow
fuente
14
No piense ni por un minuto que los errores son del dominio de los programadores junior. El 95% de los desarrolladores de todos los niveles de experiencia hacen algo de vez en cuando. El 5% restante está mintiendo entre dientes.
Blrfl
56
Si vi a alguien escribir un código que verifica un valor nulo y falla silenciosamente, me gustaría despedirlo en el acto.
Winston Ewert
16
Las cosas que fallan en silencio es casi lo peor. Al menos cuando explota sabes dónde se produce la explosión. Verificar nully no hacer nada es solo una forma de mover el error hacia abajo en la ejecución, lo que hace que sea mucho más difícil rastrear hasta la fuente.
MirroredFate
1
+1, pregunta muy interesante. Además de mi respuesta, también señalaría que cuando tiene que escribir código cuyo propósito es acomodar una prueba unitaria, lo que realmente está haciendo es intercambiar un mayor riesgo de una falsa sensación de seguridad (más líneas de código = más posibilidades de errores). El rediseño para usar referencias no solo mejora la legibilidad del código, sino que también reduce la cantidad de código (y pruebas) que debe escribir para demostrar que funciona.
Septiembre
66
@Rig, estoy seguro de que hay casos apropiados para una falla silenciosa. Pero si alguien calla los fallos como práctica general (a menos que trabaje en un ámbito donde tenga sentido), realmente no quiero estar trabajando en un proyecto en el que están poniendo código.
Winston Ewert

Respuestas:

66

Depende del 'contrato':

Si PowerManager DEBE tener un valor válido IMsgSender, nunca verifique si es nulo, déjelo morir antes.

Si, por otro lado, PUEDE tener un IMsgSender, entonces debe verificar cada vez que lo use, tan simple como eso.

Comentario final sobre la historia del programador junior, el problema es en realidad la falta de procedimientos de prueba.

aaronps
fuente
44
+1 para una respuesta muy sucinta que resume bastante bien la forma en que me siento: "depende ..." También tuvo las agallas para decir "nunca verificar si es nulo" ( si nulo no es válido). Colocar una función assert()en cada miembro que desreferencia un puntero es un compromiso que probablemente aplacará a mucha gente, pero incluso eso me parece una 'paranoia especulativa'. La lista de cosas más allá de mi capacidad de control es infinita, y una vez que me permito comenzar a preocuparme por ellas, se convierte en una pendiente realmente resbaladiza. Aprender a confiar en mis pruebas, mis colegas y mi depurador me ha ayudado a dormir mejor por la noche.
evadeflow
2
Cuando está leyendo el código, esas afirmaciones pueden ser muy útiles para comprender cómo se supone que funciona el código. Nunca me he encontrado con una base de código que me haya hecho decir "Ojalá no tuviera todas estas afirmaciones por todas partes", pero he visto muchas cosas en las que dije lo contrario.
Kristof Provost el
1
Claro y simple, esta es la respuesta correcta a la pregunta.
Matthew Azkimov el
2
Esta es la respuesta correcta más cercana, pero no puedo estar de acuerdo con 'nunca verificar nulo', siempre verifique los parámetros no válidos en el punto en que se asignarán a una variable miembro.
James
Gracias por los comentarios. Es mejor dejar que se bloquee antes si alguien no lee la especificación y la inicializa con un valor nulo cuando debe tener algo válido allí.
aaronps
77

Siento que el código debería leer:

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

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

En realidad, esto es mejor que proteger el NULL, porque deja muy claro que la función nunca debería llamarse si msgSender_es NULL. También se asegura de que notarás si esto sucede.

La "sabiduría" compartida de sus colegas ignorará silenciosamente este error, con resultados impredecibles.

En general, los errores son más fáciles de corregir si se detectan más cerca de su causa. En este ejemplo, la protección NULL propuesta llevaría a que no se establezca un mensaje de apagado, lo que puede dar como resultado un error notable. Te resultaría más difícil trabajar hacia atrás para la SignalShutdownfunción que si toda la aplicación acabara de morir, produciendo un seguimiento hacia atrás o un volcado del núcleo apuntando directamente SignalShutdown().

Es un poco contradictorio, pero fallar tan pronto como algo está mal tiende a hacer que su código sea más robusto. Esto se debe a que realmente encuentra los problemas y tiende a tener causas muy obvias también.

Kristof Provost
fuente
10
La gran diferencia entre llamar a afirmación y el ejemplo de código del OP es que la afirmación solo está habilitada en las compilaciones de depuración (#define NDEBUG) cuando el producto llega a las manos del cliente, y la depuración está deshabilitada, y una combinación de rutas de código nunca ejecutada se ejecuta para salir el puntero es nulo aunque se ejecute la función en cuestión, la afirmación no le proporciona protección.
atk
17
Presumiblemente habría probado la compilación antes de enviarla al cliente, pero sí, este es un posible riesgo. Sin embargo, las soluciones son fáciles: ya sea que se envíen con afirmaciones habilitadas (esa es la versión que probó de todos modos) o reemplácelas con su propia macro que afirma en modo de depuración y solo registros, registros + rastreos, salidas, ... en modo de lanzamiento.
Kristof Provost el
77
@James: en el 90% de los casos, tampoco se recuperará de la comprobación NULL fallida. Pero en tal caso, el código fallará silenciosamente y el error puede aparecer mucho más tarde, por ejemplo, pensó que guardó en el archivo, pero en realidad la verificación nula a veces falla y no es más sabio. No puede recuperarse de toda situación que no debe suceder. Puede verificar que no sucederán, pero no creo que tenga un presupuesto a menos que escriba un código para la central nuclear o satélite de la NASA. Es necesario tener una manera de decir "No tengo idea de lo que está sucediendo, se supone que el programa no está en este estado".
Maciej Piechotka
66
@ James No estoy de acuerdo con tirar. Eso hace que parezca que es algo que realmente puede suceder. Las afirmaciones son para cosas que se supone que son imposibles. Para errores reales en el código en otras palabras. Las excepciones son para cuando inesperado, pero posible, las cosas suceden. Las excepciones son para cosas de las que podría manejar y recuperarse. Errores de lógica en el código del que no puede recuperarse, y tampoco debe intentarlo.
Kristof Provost
2
@James: En mi experiencia con los sistemas embebidos, el software y el hardware están invariablemente diseñados de tal manera que es extremadamente difícil dejar un dispositivo completamente inutilizable. En la gran mayoría de los casos, hay un perro guardián de hardware que fuerza un reinicio completo del dispositivo si el software deja de funcionar.
Bart van Ingen Schenau
30

Si msgSender nunca debe serlo null, debe colocar el nullcheque solo en el constructor. Esto también es cierto para cualquier otra entrada en la clase: coloque la verificación de integridad en el punto de entrada al 'módulo': clase, función, etc.

Mi regla general es realizar comprobaciones de integridad entre los límites del módulo, la clase en este caso. Además, una clase debe ser lo suficientemente pequeña como para que sea posible verificar mentalmente rápidamente la integridad de las vidas de los miembros de la clase, asegurando que se eviten errores tales como eliminaciones incorrectas / asignaciones nulas. La comprobación nula que se realiza en el código de su publicación supone que cualquier uso no válido en realidad asigna nulo al puntero, lo que no siempre es el caso. Dado que el "uso no válido" implica de manera inherente que no se aplican las suposiciones sobre el código regular, no podemos estar seguros de detectar todos los tipos de errores de puntero, por ejemplo, una eliminación, incremento, etc.

Además, si está seguro de que el argumento nunca puede ser nulo, considere usar referencias, dependiendo de su uso de la clase. De lo contrario, considere usar std::unique_ptro std::shared_ptren lugar de un puntero sin formato.

Max
fuente
11

No, no es razonable verificar todas y cada una de las desreferencias de puntero para el puntero NULL.

Las comprobaciones de puntero nulo son valiosas en los argumentos de función (incluidos los argumentos del constructor) para garantizar que se cumplan las condiciones previas o para tomar las medidas adecuadas si no se proporciona un parámetro opcional, y son valiosas para comprobar la invariancia de una clase después de haber expuesto los elementos internos de la clase. Pero si la única razón para que un puntero se haya convertido en NULL es la existencia de un error, entonces no tiene sentido comprobarlo. Ese error podría fácilmente haber configurado el puntero a otro valor no válido.

Si me enfrentara a una situación como la suya, haría dos preguntas:

  • ¿Por qué el error de ese programador junior no se detectó antes del lanzamiento? Por ejemplo, en una revisión de código o en la fase de prueba? Llevar al mercado un dispositivo electrónico de consumo defectuoso puede ser tan costoso (si se tiene en cuenta la participación en el mercado y la buena voluntad) como liberar un dispositivo defectuoso utilizado en una aplicación crítica para la seguridad, por lo que esperaría que la empresa se tomara en serio las pruebas y otras actividades de control de calidad.
  • Si la verificación nula falla, ¿qué manejo de errores esperas que ponga en práctica, o podría simplemente escribir en assert(msgSender_)lugar de la verificación nula? Si solo ingresó la verificación nula, es posible que haya evitado un bloqueo, pero podría haber creado una situación peor porque el software continúa bajo la premisa de que se ha producido una operación mientras que en realidad esa operación se omitió. Esto puede provocar que otras partes del software se vuelvan inestables.
Bart van Ingen Schenau
fuente
9

Este ejemplo parece ser más sobre la vida útil del objeto que si un parámetro de entrada es nulo o no †. Como usted menciona que siemprePowerManager debe tener un valor válido , pasar el argumento por puntero (lo que permite la posibilidad de un puntero nulo) me parece un defecto de diseño ††.IMsgSender

En situaciones como esta, preferiría cambiar la interfaz para que el idioma haga cumplir los requisitos de la persona que llama:

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

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

Reescribirlo de esta manera dice que PowerManagerdebe contener una referencia IMsgSenderpara toda su vida útil. Esto, a su vez, también establece un requisito implícito que IMsgSenderdebe vivir más tiempo que PowerManager, y niega la necesidad de cualquier verificación de puntero nulo o aserciones dentro PowerManager.

También puede escribir lo mismo usando un puntero inteligente (a través de boost o c ++ 11), para forzar explícitamente IMsgSendera vivir más 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()");
}

Se prefiere este método si es posible que IMsgSenderno se pueda garantizar que la vida útil sea mayor que PowerManagerla de (es decir, x = new IMsgSender(); p = new PowerManager(*x);).

† Con respecto a los punteros: la comprobación nula desenfrenada hace que el código sea más difícil de leer y no mejora la estabilidad (mejora la apariencia de la estabilidad, que es mucho peor).

En algún lugar, alguien obtuvo una dirección de memoria para guardar el IMsgSender. Es responsabilidad de esa función asegurarse de que la asignación se realizó correctamente (verificar los valores de retorno de la biblioteca o manejar adecuadamente las std::bad_allocexcepciones), a fin de no pasar punteros inválidos.

Como PowerManagerno posee el IMsgSender(solo lo toma prestado por un tiempo), no es responsable de asignar o destruir esa memoria. Esta es otra razón por la que prefiero la referencia.

†† Como eres nuevo en este trabajo, espero que estés pirateando el código existente. Entonces, por falla de diseño, quiero decir que la falla está en el código con el que está trabajando. Por lo tanto, las personas que están marcando su código porque no está buscando punteros nulos realmente se están marcando para escribir código que requiere punteros :)

Seth
fuente
1
En realidad, a veces no hay nada de malo en usar punteros en bruto. Std :: shared_ptr no es una bala de plata y usarlo en una lista de argumentos es una cura para la ingeniería descuidada, aunque me doy cuenta de que se considera 'leet' usarlo siempre que sea posible. Usa Java si te sientes así.
James
2
¡No dije que los punteros crudos son malos! Definitivamente tienen su lugar. Sin embargo, esto es C + + , referencias (y punteros compartidos, ahora) son parte del lenguaje por una razón. Úsalos, te harán tener éxito.
Septiembre
Me gusta la idea del puntero inteligente, pero no es una opción en este concierto en particular. +1 por recomendar el uso de referencias (como lo han hecho @Max y algunas otras). A veces no es posible inyectar una referencia, sin embargo, debido a problemas de dependencia de pollo y el huevo, es decir, si un objeto que de otro modo sería un candidato para la inyección debe tener un puntero (o de referencia) a su soporte inyectado en ella . Esto a veces puede indicar un diseño estrechamente acoplado; pero a menudo es aceptable, como en la relación entre un objeto y su iterador, donde nunca tiene sentido usar el último sin el primero.
evadeflow
8

Al igual que las excepciones, las condiciones de protección solo son útiles si sabe qué hacer para recuperarse del error o si desea dar un mensaje de excepción más significativo.

La ingestión de un error (ya sea como una excepción o un control de guardia) es lo correcto cuando el error no importa. El lugar más común para ver los errores que se tragan es el código de registro de errores: no desea bloquear una aplicación porque no pudo registrar un mensaje de estado.

Cada vez que se llama a una función, y no es un comportamiento opcional, debe fallar en voz alta, no en silencio.

Editar: al pensar en su historia de programador junior, parece que lo que sucedió fue que un miembro privado se estableció como nulo cuando se suponía que eso nunca debería suceder. Tuvieron un problema con la escritura inapropiada y están intentando solucionarlo validando al leer. Esto es al revés. Cuando lo identifica, el error ya ha sucedido. La solución ejecutiva de revisión de código / compilador para eso no es condiciones de guardia, sino captadores y establecedores o miembros constantes.

jmoreno
fuente
7

Como otros han señalado, esto depende de si msgSenderpuede ser legítimamente o no NULL. Lo siguiente asume que nunca debería ser NULL.

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

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

La "solución" propuesta por los demás en su equipo viola el principio de los Programas muertos No digas mentiras . Los errores son realmente difíciles de encontrar. Un método que cambia silenciosamente su comportamiento en función de un problema anterior, no solo hace que sea difícil encontrar el primer error, sino que también agrega un segundo error propio.

El joven causó estragos al no verificar un valor nulo. ¿Qué pasa si este código causa estragos al continuar ejecutándose en un estado indefinido (el dispositivo está encendido pero el programa "piensa" que está apagado)? Quizás otra parte del programa hará algo que solo sea seguro cuando el dispositivo esté apagado.

Cualquiera de estos enfoques evitará fallas silenciosas:

  1. Use afirmaciones como lo sugiere esta respuesta , pero asegúrese de que estén activadas en el código de producción. Esto, por supuesto, podría causar problemas si se escribieran otras afirmaciones con el supuesto de que estarían fuera de producción.

  2. Lanza una excepción si es nulo.

meter
fuente
5

Estoy de acuerdo con atrapar el nulo en el constructor. Además, si el miembro se declara en el encabezado como:

IMsgSender* const msgSender_;

Entonces el puntero no se puede cambiar después de la inicialización, por lo que si estuvo bien en la construcción, estará bien durante la vida útil del objeto que lo contiene. (El objeto apuntado a será no ser const.)

Grimm The Opiner
fuente
Ese es un gran consejo, gracias! Tan bueno, de hecho, que lo siento, no puedo votarlo más alto. No es una respuesta directa a la pregunta planteada, pero es una excelente manera de eliminar cualquier posibilidad de que el puntero se convierta "accidentalmente" NULL.
evadeflow
@evadeflow Pensé "Estoy de acuerdo con todos los demás" respondió la idea principal de la pregunta. Saludos sin embargo! ;-)
Grimm The Opiner
Sería un poco descortés de mi parte cambiar la respuesta aceptada en este punto, dada la forma en que se formuló la pregunta. Pero realmente creo que este consejo es sobresaliente, y lamento no haberlo pensado yo mismo. Con un constpuntero, no hay necesidad de assert()salir del constructor, por lo que esta respuesta parece incluso un poco mejor que la de @Kristof Provost (que también fue sobresaliente, pero también abordó la pregunta indirectamente). Espero que otros voten por esto, ya que realmente no tiene sentido asserten todos los métodos cuando solo puedes hacer el puntero const.
evadeflow
@evadeflow La gente solo visita Questins para el representante, ahora se ha respondido que nadie lo volverá a ver. ;-) Solo entré porque era una pregunta sobre punteros y estoy atento a la sangrienta brigada "Use punteros inteligentes para todo siempre".
Grimm The Opiner
3

¡Esto es absolutamente peligroso!

Trabajé bajo un desarrollador sénior en una base de código C con los "estándares" de mala calidad que presionaron por lo mismo, para verificar ciegamente todos los punteros en busca de nulos. El desarrollador terminaría haciendo cosas como esta:

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

Una vez intenté eliminar esa comprobación de la condición previa una vez en dicha función y reemplazarla con una assertpara ver qué pasaría.

Para mi horror, encontré miles de líneas de código en la base de código que pasaban nulos a esta función, pero donde los desarrolladores, probablemente confundidos, trabajaron y simplemente agregaron más código hasta que las cosas funcionaron.

Para mi mayor horror, descubrí que este problema prevalecía en todo tipo de lugares en la base de código buscando nulos. La base de código había crecido durante décadas para confiar en estas comprobaciones y poder violar silenciosamente incluso las condiciones previas más explícitamente documentadas. Al eliminar estos controles mortales a favor asserts, se revelarían todos los errores humanos lógicos durante décadas en la base de código, y nos ahogaríamos en ellos.

Solo tomó dos líneas de código aparentemente inocentes como este + tiempo y un equipo para terminar enmascarando mil errores acumulados.

Estos son los tipos de prácticas que hacen que los errores dependan de la existencia de otros errores para que el software funcione. Es un escenario de pesadilla . También hace que todos los errores lógicos relacionados con la violación de tales condiciones previas aparezcan misteriosamente a un millón de líneas de código fuera del sitio real en el que ocurrió el error, ya que todas estas comprobaciones nulas simplemente ocultan el error y lo ocultan hasta que llegamos a un lugar olvidado para esconder el error.

Para mí, simplemente verificar nulos a ciegas en cada lugar donde un puntero nulo viola una condición previa es, para mí, una locura absoluta, a menos que su software sea tan crítico para la misión contra fallas de afirmación y fallas de producción que el potencial de este escenario sea preferible.

¿Es razonable que un estándar de codificación requiera que cada puntero desreferenciado en una función se verifique primero para NULL, incluso los miembros de datos privados?

Entonces diría, absolutamente no. Ni siquiera es "seguro". Puede ser todo lo contrario y enmascarar todo tipo de errores en toda su base de código que, a lo largo de los años, puede conducir a los escenarios más horribles.

assertEs el camino a seguir aquí. No se debe permitir que las violaciones de las condiciones previas pasen desapercibidas, de lo contrario, la ley de Murphy puede entrar en vigencia fácilmente.


fuente
1
"afirmar" es el camino a seguir, pero recuerde que en cualquier sistema moderno, cada acceso a la memoria a través de un puntero tiene una afirmación de puntero nulo integrada en el hardware. Entonces, en "afirmar (p! = NULL); devolver * p;" incluso la afirmación es en su mayoría inútil.
gnasher729
@ gnasher729 Ah, ese es un muy buen punto, pero tiendo a verlo assertcomo un mecanismo de documentación para establecer cuáles son las condiciones previas y no simplemente una forma de forzar un aborto. Pero también me gusta la naturaleza de la falla de aserción que muestra exactamente qué línea de código causó una falla y la condición de aserción falló ("documentar el bloqueo"). Puede ser útil si terminamos usando una compilación de depuración fuera de un depurador y quedamos desprevenidos.
@ gnasher729 O puede que haya entendido mal una parte de ella. ¿Estos sistemas modernos muestran en qué línea de código fuente falló la afirmación? En general, me imagino que perdieron la información en ese momento y podrían simplemente mostrar una violación de acceso / seguridad, pero estoy un poco lejos de ser "moderno", todavía tercamente en Windows 7 durante la mayor parte de mi desarrollo.
Por ejemplo, en MacOS X e iOS, si su aplicación se bloquea debido a un acceso de puntero nulo durante el desarrollo, el depurador le dirá la línea exacta de código donde sucede. Hay otro aspecto extraño: el compilador intentará darte advertencias si haces algo sospechoso. Si pasa un puntero a una función y accede a ella, el compilador no le dará una advertencia de que el puntero podría ser NULL, ya que sucede tan a menudo que se ahogaría en advertencias. Sin embargo, si hace una comparación if (p == NULL) ... entonces el compilador supone que hay una buena posibilidad de que p sea NULL,
gnasher729
porque por qué lo habrías probado de otra manera, por lo tanto, te dará una advertencia a partir de ese momento si lo usas sin probarlo. Si usa su propia macro de aserción, debe ser un poco inteligente para escribirla de manera que "my_assert (p! = NULL," ¡Es un puntero nulo, estúpido! "); * P = 1;" No te da una advertencia.
gnasher729
2

Objective-C , por ejemplo, trata cada llamada a un método en un nilobjeto como un no-op que se evalúa como un valor cero-ish. Hay algunas ventajas en esa decisión de diseño en Objective-C, por las razones sugeridas en su pregunta. El concepto teórico de protección nula en cada llamada a un método tiene algún mérito si se publicita bien y se aplica de manera consistente.

Dicho esto, el código vive en un ecosistema, no en un vacío. El comportamiento de protección nula sería no idiomático y sorprendente en C ++ y, por lo tanto, debería considerarse dañino. En resumen, nadie escribe código C ++ de esa manera, ¡así que no lo hagas! Sin embargo, como contraejemplo, tenga en cuenta que llamar free()o deleteen un NULLen C y C ++ está garantizado como no operativo.


En su ejemplo, probablemente valdría la pena colocar una aserción en el constructor que msgSenderno sea nula. Si el constructor llamó a un método de msgSenderinmediato, entonces no sería necesaria tal afirmación, ya que de todos modos se bloquearía allí. Sin embargo, dado que simplemente se almacena msgSender para uso futuro, no sería obvio al observar un rastro de la pila de SignalShutdown()cómo llegó a ser el valor NULL, por lo que una afirmación en el constructor facilitaría significativamente la depuración.

Aún mejor, el constructor debería aceptar una const IMsgSender&referencia, que posiblemente no puede ser NULL.

200_success
fuente
1

La razón por la que se le pide que evite las desreferencias nulas es para asegurarse de que su código sea sólido. Los ejemplos de programadores junior hace mucho tiempo son solo ejemplos. Cualquiera puede descifrar el código por accidente y causar una desreferencia nula, especialmente para los globales y los globales de la clase. En C y C ++ es aún más posible accidentalmente, con la capacidad de gestión de memoria directa. Puede que se sorprenda, pero este tipo de cosas suceden muy a menudo. Incluso por desarrolladores muy expertos, muy experimentados y muy experimentados.

No necesita verificar nulo todo, pero sí debe protegerse contra las desreferencias que tienen una probabilidad decente de ser nulo. Esto es comúnmente cuando se asignan, usan y desreferencian en diferentes funciones. Es posible que una de las otras funciones se modifique y rompa su función. También es posible que una de las otras funciones se llame fuera de servicio (por ejemplo, si tiene un repartidor que se puede llamar por separado del destructor).

Prefiero el enfoque que sus compañeros de trabajo le están diciendo en combinación con el uso de afirmar. Bloqueo en el entorno de prueba, por lo que es más obvio que hay un problema para solucionar y fallar con gracia en la producción.

También debe usar una herramienta robusta de corrección de código como la cobertura o la fortificación. Y debe abordar todas las advertencias del compilador.

Editar: como otros han mencionado, fallar silenciosamente, como en varios de los ejemplos de código, generalmente también es lo incorrecto. Si su función no puede recuperarse del valor nulo, debería devolver un error (o lanzar una excepción) a su llamador. La persona que llama es responsable de corregir su orden de llamada, recuperar o devolver un error (o lanzar una excepción) a la persona que llama, y ​​así sucesivamente. Eventualmente, una función puede recuperarse y avanzar con gracia, recuperarse y fallar con gracia (como una base de datos que falla una transacción debido a un error interno para un usuario pero no se cierra de forma aguda), o la función determina que el estado de la aplicación está dañado e irrecuperable y la aplicación se cierra.

atk
fuente
+1 por tener el coraje de apoyar una posición (aparentemente) impopular. Me hubiera decepcionado si nadie defendiera a mis colegas en esto (son personas muy inteligentes que han estado sacando un producto de calidad durante muchos años). También me gusta la idea de que las aplicaciones deberían "bloquearse en el entorno de prueba ... y fallar con gracia en la producción". No estoy convencido de que obligar a realizar NULLcontroles "de memoria" sea ​​la forma de hacerlo, pero agradezco escuchar una opinión de alguien fuera de mi lugar de trabajo que básicamente dice: "Aigh. No parece demasiado irrazonable".
evadeflow
Me molesta cuando veo personas que prueban NULL después de malloc (). Me dice que no entienden cómo los sistemas operativos modernos administran la memoria.
Kristof Provost el
2
Supongo que @KristofProvost está hablando de sistemas operativos que usan sobrecompromiso, para los cuales malloc siempre tiene éxito (pero el sistema operativo puede matar su proceso más adelante si no hay suficiente memoria disponible). Sin embargo, esta no es una buena razón para omitir las verificaciones nulas en malloc: el exceso de confirmación no es universal en todas las plataformas e incluso si está habilitado, puede haber otras razones para que malloc falle (por ejemplo, en Linux, un límite de espacio de direcciones de proceso establecido con ulimit )
John Bartholomew
1
Lo que dijo John De hecho, estaba hablando de un exceso de compromiso. Overcommit está habilitado de forma predeterminada en Linux (que es lo que paga mis facturas). No sé, pero sospecho que sí, si es lo mismo en Windows. En la mayoría de los casos, terminará fallando de todos modos, a menos que esté preparado para escribir una gran cantidad de código que nunca, nunca será probado para manejar errores que casi nunca sucederán ...
Kristof Provost
2
Permítanme agregar que también nunca he visto código (fuera del núcleo de Linux, donde la falta de memoria es realmente posible) que realmente manejará correctamente una condición de falta de memoria. Todo el código que he visto probar se bloqueará más tarde de todos modos. Todo lo que se logró fue perder el tiempo, hacer que el código sea más difícil de entender y ocultar el verdadero problema. Las desreferencias nulas son fáciles de depurar (si tiene un archivo central).
Kristof Provost el
1

El uso de un puntero en lugar de una referencia me diría que msgSenderes solo una opción y que aquí la verificación nula sería correcta. El fragmento de código es demasiado lento como para decidir eso. Tal vez hay otros elementos PowerManagerque son valiosos (o comprobables) ...

Al elegir entre el puntero y la referencia, sopesé minuciosamente ambas opciones. Si tengo que usar un puntero para un miembro (incluso para miembros privados), tengo que aceptar el if (x)procedimiento cada vez que lo desreferencia.

Lobo
fuente
Como mencioné en otro comentario en alguna parte, hay momentos en los que no es posible inyectar una referencia . Un ejemplo con el que me encuentro es que el objeto en sí necesita una referencia al titular:
evadeflow
@evadeflow: OK, lo confieso, depende del tamaño de la clase y la visibilidad de los miembros del puntero (que no se pueden hacer referencias), si verifico antes de desreferenciar. En los casos en que la clase es pequeña y simple y los punteros son privados, yo no ...
Wolf
0

Depende de lo que quieras que pase.

Usted escribió que está trabajando en "un dispositivo de electrónica de consumo". Si, de alguna manera, un error se introduce mediante el establecimiento msgSender_de NULL, ¿quiere

  • el dispositivo para continuar, omitiendo SignalShutdownpero continuando el resto de su operación, o
  • ¿El dispositivo se bloquea y obliga al usuario a reiniciarlo?

Dependiendo del impacto de la señal de apagado que no se envía, la opción 1 podría ser una opción viable. Si el usuario puede continuar escuchando su música, pero la pantalla aún muestra el título de la pista anterior, eso podría ser preferible a un bloqueo completo del dispositivo.

Por supuesto, si elige la opción 1, un assert(según lo recomendado por otros) es vital para reducir la probabilidad de que un error de este tipo pase desapercibido durante el desarrollo. La ifprotección nula está ahí para mitigar fallas en el uso de producción.

Personalmente, también prefiero el enfoque de "bloqueo anticipado" para las compilaciones de producción, pero estoy desarrollando un software empresarial que puede repararse y actualizarse fácilmente en caso de error. Para dispositivos electrónicos de consumo, esto podría no ser tan fácil.

Heinzi
fuente