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 NULL
guardia 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ó NULL
despué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 NULL
verificar 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 NULL
primero, 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 PowerManager
puede probarse con una IMsgSender
subclase 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:
- Los
NULL
guardias obligatorios para cada puntero desreferenciado son excesivos, pero - Puede esquivar todo el debate utilizando una referencia en su lugar (si es posible) o un
const
puntero, y assert
Las declaraciones son una alternativa más ilustrada que losNULL
guardias para verificar que se cumplen las condiciones previas de una función.
fuente
null
y 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.Respuestas:
Depende del 'contrato':
Si
PowerManager
DEBE tener un valor válidoIMsgSender
, 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.
fuente
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.Siento que el código debería leer:
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
SignalShutdown
función que si toda la aplicación acabara de morir, produciendo un seguimiento hacia atrás o un volcado del núcleo apuntando directamenteSignalShutdown()
.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.
fuente
Si msgSender nunca debe serlo
null
, debe colocar elnull
cheque 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_ptr
ostd::shared_ptr
en lugar de un puntero sin formato.fuente
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:
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.fuente
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 siempre
PowerManager
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:
Reescribirlo de esta manera dice que
PowerManager
debe contener una referenciaIMsgSender
para toda su vida útil. Esto, a su vez, también establece un requisito implícito queIMsgSender
debe vivir más tiempo quePowerManager
, y niega la necesidad de cualquier verificación de puntero nulo o aserciones dentroPowerManager
.También puede escribir lo mismo usando un puntero inteligente (a través de boost o c ++ 11), para forzar explícitamente
IMsgSender
a vivir más quePowerManager
:Se prefiere este método si es posible que
IMsgSender
no se pueda garantizar que la vida útil sea mayor quePowerManager
la 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 lasstd::bad_alloc
excepciones), a fin de no pasar punteros inválidos.Como
PowerManager
no posee elIMsgSender
(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 :)
fuente
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.
fuente
Como otros han señalado, esto depende de si
msgSender
puede ser legítimamente o noNULL
. Lo siguiente asume que nunca debería ser NULL.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:
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.
Lanza una excepción si es nulo.
fuente
Estoy de acuerdo con atrapar el nulo en el constructor. Además, si el miembro se declara en el encabezado como:
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.)
fuente
NULL
.const
puntero, no hay necesidad deassert()
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 sentidoassert
en todos los métodos cuando solo puedes hacer el punteroconst
.¡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:
Una vez intenté eliminar esa comprobación de la condición previa una vez en dicha función y reemplazarla con una
assert
para 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.
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.
assert
Es 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
assert
como 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.Objective-C , por ejemplo, trata cada llamada a un método en un
nil
objeto 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()
odelete
en unNULL
en C y C ++ está garantizado como no operativo.En su ejemplo, probablemente valdría la pena colocar una aserción en el constructor que
msgSender
no sea nula. Si el constructor llamó a un método demsgSender
inmediato, entonces no sería necesaria tal afirmación, ya que de todos modos se bloquearía allí. Sin embargo, dado que simplemente se almacenamsgSender
para uso futuro, no sería obvio al observar un rastro de la pila deSignalShutdown()
cómo llegó a ser el valorNULL
, 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 serNULL
.fuente
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.
fuente
NULL
controles "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".El uso de un puntero en lugar de una referencia me diría que
msgSender
es 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 elementosPowerManager
que 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.fuente
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_
deNULL
, ¿quiereSignalShutdown
pero continuando el resto de su operación, oDependiendo 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. Laif
protecció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.
fuente