¿No "siempre inicializa las variables" lleva a que se oculten errores importantes?

35

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.

Abyx
fuente
10
Aunque creo que esta es una buena pregunta, no entiendo tu ejemplo. Si se produce un error de lectura y bytes_readno 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ícitamente bytes_read!=0después. Así que está bien, los desinfectantes no se quejan. Por otro lado, cuando bytes_readno se inicializa de antemano, el programa no podrá continuar de una manera sensata, por lo que no inicializar bytes_readrealmente introduce un error que no estaba allí de antemano.
Doc Brown
2
@Abyx: incluso si se trata de un tercero, si no se trata de un búfer que comienza con \0un error. Si está documentado para no tratar con eso, su código de llamada tiene errores. Si corrige su código de llamada para verificar bytes_read==0antes de llamar al uso, entonces está de regreso al lugar donde comenzó: su código tiene errores si no se inicializa bytes_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.)
Mat
1
¿Hay alguna razón por la cual este código ignora el err_tdevuelto por my_read()? Si hay un error en alguna parte del ejemplo, eso es todo.
Blrfl
1
Es fácil: solo inicialice las variables si es significativo. Si no es así, no lo hagas. Sin embargo, estoy de acuerdo en que usar datos "ficticios" para hacerlo es malo, ya que oculta errores.
Pieter B
1
"Hay otra regla: si la ejecución del programa encuentra un error, el programa debe morir lo antes posible. No es necesario mantenerlo con vida, simplemente colgar, escribir un volcado, dárselo a los ingenieros para que lo investiguen". software de control Buena suerte recuperando el vertedero de los restos del avión.
Giorgio

Respuestas:

44

Su razonamiento va mal en varias cuentas:

  1. Las fallas de segmentación están lejos de suceder. El uso de una variable no inicializada da como resultado un comportamiento indefinido . Las fallas de segmentación son una de las formas en que tal comportamiento puede manifestarse, pero parece que parece normal.
  2. Los compiladores nunca llenan la memoria no inicializada con un patrón definido (como 0xCD). Esto es algo que algunos depuradores hacen para ayudarlo a encontrar lugares donde se utilizan variables no inicializadas. Si ejecuta dicho programa fuera de un depurador, la variable contendrá basura completamente aleatoria. Es igualmente probable que un contador como el bytes_readtenga el valor 10que tiene el valor 0xcdcdcdcd.
  3. Incluso si está ejecutando en un depurador que establece la memoria no inicializada en un patrón fijo, solo lo hacen al inicio. Esto significa que este mecanismo solo funciona de manera confiable para las variables estáticas (y posiblemente asignadas al montón). Para las variables automáticas, que se asignan en la pila o viven solo en un registro, hay muchas posibilidades de que la variable se almacene en una ubicación que se utilizó anteriormente, por lo que el patrón de memoria reveladora ya se ha sobrescrito.

La idea detrás de la guía para siempre inicializar variables es habilitar estas dos situaciones

  1. 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.

  2. La variable contiene un valor definido que puede probar para más adelante, para saber si una función como my_readha actualizado el valor. Sin inicialización, no puede saber si bytes_readrealmente tiene un valor válido, porque no puede saber con qué valor comenzó.

Bart van Ingen Schenau
fuente
8
1) se trata de probabilidades, como 1% vs 99%. 2 y 3) VC ++ genera dicho código de inicialización, también para variables locales. 3) las variables estáticas (globales) siempre se inicializan con 0.
Abyx
55
@Abyx: 1) En mi experiencia, la probabilidad es ~ 80% "no hay diferencia de comportamiento obvia inmediatamente", 10% "hace lo incorrecto", 10% "segfault". En cuanto a (2) y (3): VC ++ hace esto solo en versiones de depuración. Confiar en eso es una idea terriblemente mala, ya que rompe selectivamente las versiones de lanzamiento y no aparece en muchas de sus pruebas.
Christian Aichinger
8
Creo que la "idea detrás de la guía" es la parte más importante de esta respuesta. La guía no le dice absolutamente que siga todas las declaraciones de variables con = 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.
GrandOpener
1
@GrandOpener Exactamente. Si no hay un valor significativo para asignar en el punto donde se declara la variable, el alcance de la variable probablemente sea incorrecto.
Kevin Krumwiede
55
"Los compiladores nunca se llenan" ¿no debería ser eso no siempre ?
CodesInChaos
25

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_readfunción tiene el contrato escrito para inicializarse bytes_readen 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 inicializar bytes_readprimero 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 la my_readfunció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_readantemano y llamar my_readdespués (con la expectativa bytes_readinicializada 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 como my_readno es 100% clara, o incluso incorrecta sobre el comportamiento en caso de error. Sin embargo, siempre que bytes_readse inicialice a cero antes de la llamada, el programa se comporta de la misma manera que si la inicialización se realizara en el interior my_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

  • desea probar si una función o bloque de código inicializa un parámetro específico
  • está 100% seguro de que la función en juego tiene un contrato donde definitivamente es incorrecto no asignar un valor a ese parámetro
  • estás 100% seguro de que el medio ambiente puede atrapar esto

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.

Doc Brown
fuente
44
Esto es exactamente lo que estaba pensando cuando lo leí. ¡No está barriendo cosas debajo de la alfombra, las está barriendo al basurero!
corsiKa
22

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:

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

He agregado más comentarios en mi versión, pero la idea es la misma. Si se put + dataLengthenvuelve, será más pequeño que el putpuntero (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 ifdeclaració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 que ptr+dataLengthnunca puede estar por debajo ptrsin un desbordamiento del puntero UB, la ifinstrucció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.

Cort Ammon - Restablece a Monica
fuente
Para una lectura muy divertida sobre comportamiento indefinido, software.intel.com/en-us/blogs/2013/01/06/… es una publicación increíblemente bien escrita sobre lo malo que puede ir. Sin embargo, esa publicación en particular es sobre operaciones atómicas, que son muy confusas para la mayoría, por lo que evito recomendarlo como una guía para UB y cómo puede salir mal.
Cort Ammon - Restablece a Monica el
1
Desearía que C tuviera intrínsecos para establecer un valor l o una matriz de ellos en valores indeterminados no inicializados, no atrapantes, o valores no especificados, o convertir valores desagradables en valores menos desagradables (no atrapados indeterminados o no especificados) mientras deja solo los valores definidos. Los compiladores podrían usar tales directivas para ayudar a optimizaciones útiles, y los programadores podrían usarlas para evitar tener que escribir código inútil mientras bloquean las "optimizaciones" de última hora cuando se usan cosas como técnicas de matriz dispersa.
supercat
@supercat Sería una buena característica, suponiendo que esté apuntando a plataformas donde esa es una solución válida. Uno de los ejemplos de problemas conocidos es la capacidad de crear patrones de memoria que no solo son inválidos para el tipo de memoria, sino que son imposibles de lograr por medios ordinarios. booles 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.
Cort Ammon - Restablece a Mónica
Considere el caso en el que un optimizador puede probar que un valor usado para a switches 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".
Cort Ammon - Restablece a Monica el
Si los intrínsecos fueran estandarizados, se podría requerir que los compiladores hicieran lo que fuera necesario para honrar la semántica; si, por ejemplo, algunas rutas de código establecen una variable y otras no, y un intrínseco luego dice "convertir a valor no especificado si no está inicializado o es indeterminado; de lo contrario, déjelo solo", un compilador para plataformas con registros de "no-un-valor" debería inserte código para inicializar la variable, ya sea antes de cualquier ruta de código, o en cualquier ruta de código donde la inicialización se perdería, pero el análisis semántico requerido para hacerlo es bastante simple.
supercat
5

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_readpruebas 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.

Karl Bielefeldt
fuente
5

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
1
E inicializar con -1 puede ser realmente significativo. Donde "int bytes_read = 0" es malo, porque en realidad puede leer 0 bytes, inicializarlo con -1 deja muy claro que ningún intento de leer bytes ha tenido éxito, y puede probarlo.
Pieter B
4

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:

Comportamiento indefinido es un contrato social entre el desarrollador y el compilador; El compilador asume con fe ciega que su usuario nunca, nunca, dependerá de Comportamiento Indefinido.

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:

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

se transforma en:

void contains_null_check(int *P) {
  *P = 4;
}

porque es obvio que Pno puede ser, 0ya que está desreferenciado antes de ser verificado.


¿Cómo se aplica esto a tu ejemplo?

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.

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_reades:

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

y proceda como se espera de un buen compilador con inlining:

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

Luego, como se esperaba de un buen compilador, optimizamos las ramas inútiles:

  1. No se debe usar ninguna variable sin inicializar
  2. bytes_readse usaría sin inicializar si resultno fuera0
  3. ¡El desarrollador promete que resultnunca lo será 0!

Entonces resultnunca es 0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Oh, resultnunca se usa:

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

Oh, podemos posponer la declaración de bytes_read:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(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.

Matthieu M.
fuente
A veces creo que los compiladores deberían hacer que el programa elimine los archivos de origen cuando ejecutan una ruta UB. Programadores y luego aprender lo que significa la UB a su usuario final ....
mattnz
1

Echemos un vistazo más de cerca a su código de ejemplo:

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);

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ínea my_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_readNo debe ser cero en el éxito. (O si puede ser, podríamos querer inicializarlo a -1.) Probablemente podamos detectar el error aquí. Sin embargo, si bytes_reades 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.

Davislor
fuente
1

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.

Jordan Brown
fuente
0

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:

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

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:

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

Creo que es una buena idea escribir estos casos así:

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

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:

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

Creo que esto es casi siempre dañino: el único efecto de estas inicializaciones es que hacen que las herramientas sean valgrindimpotentes. 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 por valgrind. 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 valgrindo 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. Ese valgrindobjetivo 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.

cmaster
fuente
Observe que puede escribir inicializaciones condicionales con el operador ternario, por ejemplo Base& b = foo() ? new Derived1 : new Derived2;
Davislor
@Lorehead Eso puede funcionar para los casos simples, pero no funcionará para los casos más complejos: no desea hacer esto si tiene tres o más casos, y sus constructores toman tres o más argumentos, simplemente para facilitar la lectura razones. Y eso ni siquiera considera ningún cálculo que deba hacerse, como buscar un argumento para una rama de inicialización en un bucle.
cmaster
Para los casos más complicados, se puede ajustar el código de inicialización en función de fábrica: 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.
Davislor
@Lorehead Eso es cierto, y ciertamente el camino a seguir si la lógica requerida no es simple. Sin embargo, creo que hay una pequeña área gris en el medio donde la inicialización vía ?:es un PITA, y una función de fábrica todavía es exagerada. Estos casos son pocos y distantes entre sí, pero existen.
cmaster
-2

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.

gnasher729
fuente
Esto suena superficialmente bien, pero depende demasiado de la precisión de las advertencias de valor no inicializado. Conseguir estos perfectamente correcto es equivalente al problema de la parada, y los compiladores de producción pueden hacer y sufrir falsos negativos (es decir, que no diagnosticar una variable no inicializada cuando deberían tener); véase, por ejemplo, el error 18501 de GCC , que lleva más de diez años sin reparar .
zwol
Lo que dices sobre gcc se acaba de decir. El resto es irrelevante.
gnasher729
Es triste sobre gcc, pero si no entiendes por qué el resto es relevante, entonces necesitas educarte.
zwol