Las Pautas principales de C ++ tienen la regla ES.20: Inicializar siempre un objeto .
Evite los errores usados antes de configurar y su comportamiento indefinido asociado. Evite problemas con la comprensión de la inicialización compleja. Simplifica la refactorización.
Pero esta regla no ayuda a encontrar errores, solo los oculta.
Supongamos que un programa tiene una ruta de ejecución donde utiliza una variable no inicializada. Es un error Dejando a un lado el comportamiento indefinido, también significa que algo salió mal y el programa probablemente no cumple con los requisitos del producto. Cuando se implementará en producción, puede haber una pérdida de dinero, o incluso peor.
¿Cómo detectamos errores? Escribimos pruebas. Pero las pruebas no cubren el 100% de las rutas de ejecución, y las pruebas nunca cubren el 100% de las entradas del programa. Más que eso, incluso una prueba cubre una ruta de ejecución defectuosa, aún puede pasar. Es un comportamiento indefinido, después de todo, una variable no inicializada puede tener un valor algo válido.
Pero además de nuestras pruebas, tenemos los compiladores que pueden escribir algo como 0xCDCDCDCD a variables no inicializadas. Esto mejora ligeramente la tasa de detección de las pruebas.
Aún mejor: hay herramientas como Address Sanitizer, que capturará todas las lecturas de bytes de memoria no inicializados.
Y finalmente hay analizadores estáticos, que pueden mirar el programa y decir que hay una lectura antes establecida en esa ruta de ejecución.
Entonces tenemos muchas herramientas poderosas, pero si inicializamos la variable, los desinfectantes no encuentran nada .
int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.
// Another bug: use empty buffer after read error.
use(buffer);
Hay otra regla: si la ejecución del programa encuentra un error, el programa debería morir lo antes posible. No es necesario mantenerlo con vida, simplemente colóquese, escriba un volcado de choque, déselo a los ingenieros para que lo investiguen.
Inicializar variables innecesariamente hace lo contrario: el programa se mantiene con vida, de lo contrario ya tendría un fallo de segmentación.
bytes_read
no se cambia (por lo que se mantiene en cero), ¿por qué se supone que esto es un error? El programa aún podría continuar de una manera sensata siempre que no espere implícitamentebytes_read!=0
después. Así que está bien, los desinfectantes no se quejan. Por otro lado, cuandobytes_read
no se inicializa de antemano, el programa no podrá continuar de una manera sensata, por lo que no inicializarbytes_read
realmente introduce un error que no estaba allí de antemano.\0
un error. Si está documentado para no tratar con eso, su código de llamada tiene errores. Si corrige su código de llamada para verificarbytes_read==0
antes de llamar al uso, entonces está de regreso al lugar donde comenzó: su código tiene errores si no se inicializabytes_read
, seguro si lo hace. ( Por lo general, se supone que las funciones llenan sus parámetros de salida incluso en caso de error : no realmente. Muy a menudo las salidas se dejan solas o sin definir.)err_t
devuelto pormy_read()
? Si hay un error en alguna parte del ejemplo, eso es todo.Respuestas:
Su razonamiento va mal en varias cuentas:
bytes_read
tenga el valor10
que tiene el valor0xcdcdcdcd
.La idea detrás de la guía para siempre inicializar variables es habilitar estas dos situaciones
La variable contiene un valor útil desde el principio de su existencia. Si combina eso con la guía para declarar una variable solo una vez que la necesita, puede evitar que los futuros programadores de mantenimiento caigan en la trampa de comenzar a usar una variable entre su declaración y la primera asignación, donde la variable existiría pero no se inicializaría.
La variable contiene un valor definido que puede probar para más adelante, para saber si una función como
my_read
ha actualizado el valor. Sin inicialización, no puede saber sibytes_read
realmente tiene un valor válido, porque no puede saber con qué valor comenzó.fuente
= 0;
. La intención del consejo es declarar la variable en el punto donde tendrá un valor útil e inmediatamente asignar este valor. Esto se hace explícitamente claro en las siguientes reglas ES21 y ES22. Esos tres deben entenderse como trabajando juntos; no como reglas individuales no relacionadas.Usted escribió "esta regla no ayuda a encontrar errores, solo los oculta" - bueno, el objetivo de la regla no es ayudar a encontrar errores, sino evitarlos . Y cuando se evita un error, no hay nada oculto.
Analicemos el problema en términos de su ejemplo: suponga que la
my_read
función tiene el contrato escrito para inicializarsebytes_read
en todas las circunstancias, pero no lo hace en caso de error, por lo que es defectuoso, al menos, para este caso. Su intención es utilizar el entorno de tiempo de ejecución para mostrar ese error al no inicializarbytes_read
primero el parámetro. Siempre y cuando sepa con certeza que hay un desinfectante de direcciones en el lugar, esa es una posible forma de detectar dicho error. Para corregir el error, uno tiene que cambiar lamy_read
función internamente.Pero hay un punto de vista diferente, que es al menos igualmente válido: el comportamiento defectuoso solo surge de la combinación de no inicializar de
bytes_read
antemano y llamarmy_read
después (con la expectativabytes_read
inicializada después de eso). Esta es una situación que sucederá a menudo en componentes del mundo real cuando la especificación escrita para una función comomy_read
no es 100% clara, o incluso incorrecta sobre el comportamiento en caso de error. Sin embargo, siempre quebytes_read
se inicialice a cero antes de la llamada, el programa se comporta de la misma manera que si la inicialización se realizara en el interiormy_read
, por lo que se comporta correctamente, en esta combinación no hay ningún error en el programa.Entonces, mi recomendación que sigue es: use el enfoque de no inicialización solo si
Estas son condiciones que normalmente puede organizar en el código de prueba , para un entorno de herramientas específico.
Sin embargo, en el código de producción, siempre es mejor inicializar dicha variable de antemano, es el enfoque más defensivo, lo que evita errores en caso de que el contrato sea incompleto o incorrecto, o en caso de que el desinfectante de direcciones o medidas de seguridad similares no estén activadas. Y la regla "crash-early" se aplica, como escribió correctamente, si la ejecución del programa encuentra un error. Pero al inicializar una variable de antemano significa que no hay nada malo, entonces no hay necesidad de detener la ejecución posterior.
fuente
Siempre inicialice sus variables
La diferencia entre las situaciones que está considerando es que el caso sin inicialización da como resultado un comportamiento indefinido , mientras que el caso en el que se tomó el tiempo para inicializar crea un error bien definido y determinista . No puedo enfatizar cuán extremadamente diferentes son estos dos casos.
Considere un ejemplo hipotético que puede haberle sucedido a un empleado hipotético en un programa de simulaciones hipotéticas. Este equipo hipotético estaba hipotéticamente tratando de hacer una simulación determinista para demostrar que el producto que hipotéticamente vendían satisfacía las necesidades.
Bien, me detendré con la palabra inyecciones. Creo que entiendes el punto ;-)
En esta simulación, había cientos de variables no inicializadas. Un desarrollador ejecutó valgrind en la simulación y notó que había varios errores de "ramificación en valor no inicializado". "Hmm, parece que eso podría causar no determinismo, haciendo que sea difícil repetir las pruebas cuando más lo necesitamos". El desarrollador fue a la administración, pero la administración estaba en un horario muy apretado y no podía ahorrar recursos para rastrear este problema. "Terminamos inicializando todas nuestras variables antes de usarlas. Tenemos buenas prácticas de codificación".
Unos meses antes de la entrega final, cuando la simulación está en modo de abandono completo, y todo el equipo está corriendo para terminar todo lo que la administración prometió con un presupuesto que, como todos los proyectos financiados, era demasiado pequeño. Alguien notó que no podían probar una característica esencial porque, por alguna razón, el simulador determinista no se comportaba de manera determinista para depurar.
Es posible que todo el equipo se haya detenido y haya pasado la mayor parte de 2 meses peinando toda la base de código de simulación arreglando errores de valor no inicializados en lugar de implementar y probar características. No es necesario decir que el empleado se saltó los "Te dije" y comenzó a ayudar a otros desarrolladores a comprender cuáles son los valores no inicializados. Curiosamente, los estándares de codificación se cambiaron poco después de este incidente, alentando a los desarrolladores a que siempre inicialicen sus variables.
Y este es el disparo de advertencia. Esta es la bala que rozó tu nariz. El problema real es mucho, mucho, mucho, mucho más insidioso de lo que imaginas.
El uso de un valor no inicializado es "comportamiento indefinido" (a excepción de algunos casos de esquina como
char
). El comportamiento indefinido (o UB para abreviar) es tan loco y completamente malo para usted, que nunca debería creer que es mejor que la alternativa. A veces puede identificar que su compilador particular define el UB, y luego es seguro de usar, pero de lo contrario, el comportamiento indefinido es "cualquier comportamiento que el compilador siente". Puede hacer algo que llamarías "cuerdo" como tener un valor no especificado. Puede emitir códigos de operación no válidos, lo que puede causar que su programa se corrompa. Puede desencadenar una advertencia en el momento de la compilación, o incluso el compilador puede considerarlo como un error absoluto.O puede que no haga nada
Mi canario en la mina de carbón para UB es un caso de un motor SQL sobre el que leí. Perdóname por no vincularlo, no he podido encontrar el artículo nuevamente. Hubo un problema de desbordamiento del búfer en el motor SQL cuando pasó un tamaño de búfer más grande a una función, pero solo en una versión particular de Debian. El error fue debidamente registrado y explorado. La parte divertida fue: se comprobó el desbordamiento del búfer . Había código para manejar el desbordamiento del búfer en su lugar. Se parecía a esto:
He agregado más comentarios en mi versión, pero la idea es la misma. Si se
put + dataLength
envuelve, será más pequeño que elput
puntero (tenían controles de tiempo de compilación para asegurarse de que int sin firmar era del tamaño de un puntero, para los curiosos). Si esto sucede, sabemos que los algoritmos de buffer de anillo estándar pueden confundirse con este desbordamiento, por lo que devolvemos 0. ¿ O sí?Como resultado, el desbordamiento en los punteros no está definido en C ++. Debido a que la mayoría de los compiladores tratan los punteros como enteros, terminamos con comportamientos típicos de desbordamiento de enteros, que resultan ser el comportamiento que queremos. Sin embargo, este es un comportamiento indefinido, lo que significa que el compilador puede hacer lo que quiera.
En el caso de este error, Debian pasó a optar por utilizar una nueva versión de gcc que ninguno de los otros grandes sabores de Linux había actualizado en sus notas de producción. Esta nueva versión de gcc tenía un optimizador de código muerto más agresivo. El compilador vio el comportamiento indefinido y decidió que el resultado de la
if
declaración sería "cualquier cosa que haga mejor la optimización del código", que fue una traducción absolutamente legal de UB. En consecuencia, asumió que, dado queptr+dataLength
nunca puede estar por debajoptr
sin un desbordamiento del puntero UB, laif
instrucción nunca se activará y optimizó la verificación de desbordamiento del búfer.El uso de UB "sano" en realidad causó que un producto SQL importante tuviera un exploit de desbordamiento de búfer que tenía un código escrito para evitar.
Nunca confíe en un comportamiento indefinido. Siempre.
fuente
bool
es un excelente ejemplo donde hay problemas obvios, pero aparecen en otro lugar a menos que presuma que está trabajando en una plataforma muy útil como x86 o ARM o MIPS donde todos estos problemas se resuelven en el momento del código de operación.switch
es menor que 8, debido a los tamaños de la aritmética de enteros, por lo que podrían usar instrucciones rápidas que suponían que no había riesgo de que entrara un valor "grande". De repente, un aparece un valor no especificado (que nunca podría construirse usando las reglas del compilador), haciendo algo inesperado, y de repente tienes un salto masivo desde el final de una tabla de salto. Permitir resultados no especificados aquí significa que cada declaración de cambio en el programa debe tener trampas adicionales para admitir estos casos que "nunca pueden ocurrir".Principalmente trabajo en un lenguaje de programación funcional en el que no puedes reasignar variables. Siempre. Eso elimina por completo esta clase de errores. Al principio, esto parecía una gran restricción, pero te obliga a estructurar tu código de manera coherente con el orden en que aprendes los nuevos datos, lo que tiende a simplificar tu código y hacer que sea más fácil de mantener.
Esos hábitos también pueden trasladarse a idiomas imperativos. Casi siempre es posible refactorizar su código para evitar inicializar una variable con un valor ficticio. Eso es lo que esas pautas le dicen que haga. Quieren que coloque algo significativo allí, no algo que simplemente haga felices las herramientas automatizadas.
Su ejemplo con una API de estilo C es un poco más complicado. En esos casos, cuando uso la función, inicializaré a cero para evitar que el compilador se queje, pero una vez en las
my_read
pruebas unitarias, inicializaré a otra cosa para asegurarme de que la condición de error funcione correctamente. No necesita probar todas las posibles condiciones de error en cada uso.fuente
No, no esconde errores. En cambio, hace que el comportamiento sea determinista de tal manera que si un usuario encuentra un error, un desarrollador puede reproducirlo.
fuente
TL; DR: Hay dos formas de hacer que este programa sea correcto, inicializando sus variables y orando. Solo uno ofrece resultados consistentemente.
Antes de poder responder a su pregunta, primero tendré que explicar qué significa Comportamiento indefinido . En realidad, dejaré que un autor del compilador haga la mayor parte del trabajo:
Si no está dispuesto a leer esos artículos, un TL; DR es:
Desafortunadamente, el arquetipo de "Demonios que vuelan desde tu nariz" no ha logrado transmitir las implicaciones de este hecho. Si bien tenía la intención de demostrar que cualquier cosa podía suceder, era tan increíble que se ignoraba en su mayoría.
La verdad, sin embargo, es que el comportamiento indefinido afecta la compilación sí, mucho antes de que incluso intente usar el programa (instrumentado o no, dentro de un depurador o no) y puede cambiar completamente su comportamiento.
Encuentro sorprendente el ejemplo de la parte 2 anterior:
se transforma en:
porque es obvio que
P
no puede ser,0
ya que está desreferenciado antes de ser verificado.¿Cómo se aplica esto a tu ejemplo?
Bueno, ha cometido el error común de suponer que un comportamiento indefinido causaría un error en tiempo de ejecución. Puede que no.
Imaginemos que la definición de
my_read
es:y proceda como se espera de un buen compilador con inlining:
Luego, como se esperaba de un buen compilador, optimizamos las ramas inútiles:
bytes_read
se usaría sin inicializar siresult
no fuera0
result
nunca lo será0
!Entonces
result
nunca es0
:Oh,
result
nunca se usa:Oh, podemos posponer la declaración de
bytes_read
:Y aquí estamos, una transformación estrictamente confirmadora del original, y ningún depurador atrapará una variable no inicializada porque no hay ninguna.
He recorrido ese camino, entender el problema cuando el comportamiento esperado y el ensamblaje no coinciden no es realmente divertido.
fuente
Echemos un vistazo más de cerca a su código de ejemplo:
Es un buen ejemplo. Si anticipamos un error como este, podemos insertar la línea
assert(bytes_read > 0);
y detectar este error en tiempo de ejecución, lo cual no es posible con una variable no inicializada.Pero supongamos que no, y encontramos un error dentro de la función
use(buffer)
. Cargamos el programa en el depurador, verificamos el rastreo y descubrimos que se invocó desde este código. Así que colocamos un punto de interrupción en la parte superior de este fragmento, corremos de nuevo y reproducimos el error. Un solo paso tratando de atraparlo.Si no lo hemos inicializado
bytes_read
, contiene basura. No necesariamente contiene la misma basura cada vez. Pasamos la líneamy_read(buffer, &bytes_read);
. Ahora, si es un valor diferente que antes, ¡es posible que no podamos reproducir nuestro error en absoluto! Podría funcionar la próxima vez, en la misma entrada, por accidente completo. Si es consistentemente cero, obtenemos un comportamiento consistente.Verificamos el valor, tal vez incluso en una traza inversa en la misma ejecución. Si es cero, podemos ver que algo está mal;
bytes_read
No debe ser cero en el éxito. (O si puede ser, podríamos querer inicializarlo a -1.) Probablemente podamos detectar el error aquí. Sin embargo, sibytes_read
es un valor plausible, eso es incorrecto, ¿lo detectaríamos de un vistazo?Esto es especialmente cierto en el caso de los punteros: un puntero NULL siempre será obvio en un depurador, puede probarse con mucha facilidad y debería fallar en el hardware moderno si tratamos de desreferenciarlo. Un puntero de basura puede causar errores irrecuperables de corrupción de memoria más tarde, y estos son casi imposibles de depurar.
fuente
El OP no se basa en un comportamiento indefinido, o al menos no exactamente. De hecho, confiar en un comportamiento indefinido es malo. Al mismo tiempo, el comportamiento de un programa en un caso inesperado también es indefinido, pero un tipo diferente de indefinido. Si se establece una variable a cero, pero que no tienen la intención de tener una ruta de ejecución que utiliza ese cero inicial, se comportan de su programa de cordura cuando se tiene un error y hacer que ese camino? Ahora estás en la maleza; no planeaste usar ese valor, pero lo estás usando de todos modos. Tal vez sea inofensivo, o tal vez haga que el programa se bloquee, o tal vez haga que el programa corrompa silenciosamente los datos. Usted no sabe
Lo que dice el OP es que hay herramientas que lo ayudarán a encontrar este error, si lo permite. Si no inicializa el valor, pero luego lo usa de todos modos, hay analizadores estáticos y dinámicos que le indicarán que tiene un error. Un analizador estático le dirá incluso antes de comenzar a probar el programa. Si, por otro lado, inicializa ciegamente el valor, los analizadores no pueden decir que no planeó usar ese valor inicial, por lo que su error no se detecta. Si tienes suerte, es inofensivo o simplemente bloquea el programa; Si tienes mala suerte, silenciosamente corrompe los datos.
El único lugar en el que no estoy de acuerdo con el OP es al final, donde dice "cuando de otro modo ya se produciría una falla de segmentación". De hecho, una variable no inicializada no generará de manera confiable una falla de segmentación. En cambio, diría que debería usar herramientas de análisis estático que no le permitirán llegar al punto de intentar ejecutar el programa.
fuente
Una respuesta a su pregunta debe desglosarse en los diferentes tipos de variables que aparecen dentro de un programa:
Variables locales
Por lo general, la declaración debe estar justo en el lugar donde la variable obtiene primero su valor. No predeclare variables como en el antiguo estilo C:
Esto elimina el 99% de la necesidad de inicialización, las variables tienen su valor final desde el principio. Las pocas excepciones son donde la inicialización depende de alguna condición:
Creo que es una buena idea escribir estos casos así:
I. e. explícitamente afirme que se realiza una inicialización sensata de su variable.
Variables miembro
Aquí estoy de acuerdo con lo que dijeron los otros respondedores: estos siempre deben ser inicializados por las listas de constructores / inicializadores. De lo contrario, es difícil garantizar la coherencia entre sus miembros. Y si tiene un conjunto de miembros que no parece necesitar inicialización en todos los casos, refactorice su clase, agregando esos miembros en una clase derivada donde siempre se necesitan.
Tampones
Aquí es donde no estoy de acuerdo con las otras respuestas. Cuando las personas se vuelven religiosas acerca de la inicialización de variables, con frecuencia terminan inicializando buffers como este:
Creo que esto es casi siempre dañino: el único efecto de estas inicializaciones es que hacen que las herramientas sean
valgrind
impotentes. Cualquier código que lea más de los buffers inicializados de lo que debería es muy probable que sea un error. Pero con la inicialización, ese error no puede ser expuesto porvalgrind
. Por lo tanto, no los use a menos que realmente confíe en que la memoria esté llena de ceros (y en ese caso, deje caer un comentario que indique para qué necesita los ceros).También recomendaría agregar un objetivo a su sistema de compilación que ejecute todo el conjunto de pruebas
valgrind
o una herramienta similar para exponer errores de uso antes de la inicialización y pérdidas de memoria. Esto es más valioso que todas las preinicializaciones de variables. Esevalgrind
objetivo debe ejecutarse de forma regular, lo más importante antes de que cualquier código se haga público.Variables globales
No puede tener variables globales que no estén inicializadas (al menos en C / C ++, etc.), así que asegúrese de que esta inicialización sea lo que desea.
fuente
Base& b = foo() ? new Derived1 : new Derived2;
Base &b = base_factory(which);
. Esto es más útil si necesita llamar al código más de una vez o si le permite hacer que el resultado sea constante.?:
es un PITA, y una función de fábrica todavía es exagerada. Estos casos son pocos y distantes entre sí, pero existen.Un compilador decente de C, C ++ u Objective-C con el conjunto de opciones de compilador correcto le dirá en tiempo de compilación si se utiliza una variable antes de establecer su valor. Dado que en estos idiomas el uso del valor de una variable no inicializada es un comportamiento indefinido, "establecer un valor antes de usar" no es una pista, una guía o una buena práctica, es un requisito del 100%; de lo contrario, su programa está completamente roto. En otros lenguajes, como Java y Swift, el compilador nunca le permitirá usar una variable antes de que se inicialice.
Hay una diferencia lógica entre "inicializar" y "establecer un valor". Si quiero encontrar la tasa de conversión entre dólares y euros, y escribir "tasa doble = 0.0;" entonces la variable tiene un conjunto de valores, pero no se inicializa. El 0.0 almacenado aquí no tiene nada que ver con el resultado correcto. En esta situación, si por un error nunca almacena la tasa de conversión correcta, el compilador no tiene la oportunidad de decírselo. Si acaba de escribir "doble tasa"; y nunca almacenó una tasa de conversión significativa, el compilador le diría.
Entonces: No inicialice una variable solo porque el compilador le dice que se usa sin inicializarse. Eso está ocultando un error. El verdadero problema es que está usando una variable que no debería usar, o que en una ruta de código no estableció un valor. Solucione el problema, no lo oculte.
No inicialice una variable solo porque el compilador podría decirle que se usa sin inicializarse. De nuevo, estás ocultando problemas.
Declarar variables cercanas al uso. Esto mejora las posibilidades de que pueda inicializarlo con un valor significativo en el punto de declaración.
Evite reutilizar variables. Cuando reutiliza una variable, lo más probable es que se inicialice a un valor inútil cuando la utiliza para el segundo propósito.
Se ha comentado que algunos compiladores tienen falsos negativos y que la comprobación de la inicialización es equivalente al problema de detención. Ambos son irrelevantes en la práctica. Si un compilador, como se cita, no puede encontrar el uso de una variable no inicializada diez años después de que se informa el error, entonces es hora de buscar un compilador alternativo. Java implementa esto dos veces; una vez en el compilador, una vez en el verificador, sin ningún problema. La manera fácil de solucionar el problema de la detención no es exigir que una variable se inicialice antes de usarse, sino que se inicializa antes de usar de una manera que pueda verificarse mediante un algoritmo simple y rápido.
fuente