Haz del mundo un lugar mejor
Tuve mi primer contacto con la programación de alto nivel con C++. Se escribió y borró código y luego se volvió a escribir, se perdieron y ganaron batallas, y extraños bugs me mantuvieron despierto durante muchas noches antes de que finalmente se arreglaran.
Voy a contarte mi historia y cómo podrías evitar algunos de los bugs más molestos con los que tuve que lidiar en mis inicios.
Público objetivo
Programadores de C++ que deseen entender mejor el lenguaje, o programadores con experiencia en otros lenguajes que quieran aprender más sobre C++.
Qué vamos a cubrir
– Advertencias del compilador
– Declaración const
– Inicialización de variables
– Métodos autogenerados por el compilador
– Especificador Override
El compilador es tu mejor amigo. No ignores las advertencias
Al principio, las advertencias del compilador me hacían sentir como: “Gracias a Dios, eso no es un error y a pesar de que está ahí, mi código sigue compilando. Uf!”.
Luego, tras experimentar algunos errores muy extraños, que me quitaron algunos de los mejores años de mi vida y me añadieron varias canas a la cabeza, me di cuenta de que las advertencias del compilador están aquí para ayudarme. En un mundo ideal, tu proceso de compilación incluiría una bandera que trata las advertencias como errores para asegurar que tu código está libre de errores tontos.
Recuerda: Cada advertencia es el resultado de que el programador haya perdido la cabeza por un error extraño. Algunos de los errores descritos a continuación, podrían haber sido fácilmente evitados si la compilación estuviera libre de advertencias.
Const es tu segundo mejor amigo. Utilícela siempre que sea posible
El especificador const puede parecer redundante al principio, porque… Bueno, ¿qué hace? Sólo anima al compilador a no darme mi binario porque he asignado un valor const a una variable no const. ¿No es así?
Pues no. A medida que tu proyecto crece, el especificador const proporciona documentación y seguridad para tu código.
Una vez tuve que depurar el estado de una variable. El código fuente era grande y difícil de seguir, y tenía que averiguar dónde podían producirse los cambios de la variable. Era un objeto que se pasaba como una referencia no-const a lo largo de todo el código fuente. En la mayoría de los lugares, se usaba para leer el valor de la variable. Sólo había tres lugares en los que la variable podia actualizarce, uno de los cuales ocurrió por error. Ese error era el fallo, y se tardó dos días en probarlo.
Si se aplicaran declaraciones const en ese código, se habrían resuelto dos problemas:
- Mi variable no habría podido actualizarse si se hubiera especificado explícitamente al compilador como de sólo lectura, también conocida como const.
- Me habría llevado cuatro minutos rastrear el error si todos los demás métodos tuvieran declaraciones const, pero en cambio tuve que recorrer la mitad del código fuente para verificar que el problema sólo existía en ese método.
Por favor, tómate tu tiempo para leer más sobre dónde puedes aplicar la declaración const.
Inicialice sus variables
Si vienes de un lenguaje donde las variables no inicializadas no existen, debes prestar atención a esto.
Si no se inicializan las variables, se podrán seguir utilizando. Algunas de ellas se inicializarán a cero, mientras que otras pueden contener simplemente el valor de lo que sea que se haya utilizado previamente en la memoria. Esto significa que la lectura de una variable no inicializada resulta en un comportamiento indefinido. Como programador, nunca querrás que eso le ocurra a tu código, porque un código lo más determinista posible es lo que siempre buscamos.
Por favor, acostúmbrese a inicializar sus variables lo más cerca posible del lugar donde las declara. Si puedes, hazlo en la misma línea. Recuerde que la inicialización es importante para todas las variables, así como la inicialización de miembros dentro de sus clases.
Su compilador genera y llama automáticamente a los métodos de la clase por usted. Sea consciente de ellos
El compilador busca varios métodos que necesita para los procedimientos más básicos cada vez que compila una clase. Si no has declarado esos métodos, los generará automáticamente. Debes ser consciente de lo que significa realmente.
¿Qué necesita el compilador y qué puede generar?
- Necesita una forma de crear tu objeto, así que si no declaraste ningún constructor, genera un constructor por defecto.
- Por supuesto, también necesita destruir tu objeto, así que si no declaraste un metodo destroit, también genera un metodo destroit para ti.
- Además, es posible que el compilador necesite copiar su objeto, por lo que genera un constructor copy y un operador de asignación si no los ha implementado.
El constructor por defecto se implementa como si hubieras inicializado todas las variables con sus valores por defecto. Para los tipos primitivos es básicamente un comportamiento indefinido. Para los tipos definidos por el usuario, se llamará a su constructor por defecto.
El metodo destroit generado llama a los metodos destroit de todas sus variables, lo que significa que si has asignado dinámicamente alguna memoria y mantienes en ella un tipo primitivo, no será borrada por un metodo destroit por defecto, lo que significa: ¡felicidades! Tienes una fuga de memoria 🙂
Los constructores copy y los operadores de asignación se implementan como si los llamaras en cada variable, lo que se llama copia superficial. En otras palabras, no se asignaría memoria dinámicamente para crear el nuevo objeto. ¿Qué ocurre si una de sus variable es un pointer? Las copias autogeneradas sólo crearían un puntero que apunta a la dirección de memoria original. Imagínate el horror si cambias el valor de un objeto sin darte cuenta de que también has cambiado el valor del otro.
Otra implementación por defecto que proporciona el compilador, es el constructor move y el operador de asignación move. Estos se implementan de la misma manera que el constructor copy y el operador de asignación, excepto que normalmente ‘roban’ los recursos del objeto del que se copian.
¿Qué puedes hacer para evitar los errores relacionados?
- Asegúrate de saber dónde el compilador puede tomar cartas en el asunto.
- Considere definir explícitamente estos métodos o utilizar el especificador por defecto para mejorar la legibilidad.
- Utilice el especificador de eliminación para no permitir la generación automática de métodos que no tiene intención de utilizar.
Por ejemplo, la clase ‘Uncopyable’ usa el especificador de eliminación para deshabilitar la generación automática del constructor de copia y el operador de asignación:
Sugerencia personal: podrías colocar esta clase en una librería ‘utils’ y heredar de ella cada vez que quieras deshabilitar la copia de una clase:
Utilice siempre el especificador Override en los métodos anulados
Una vez tuve un error realmente molesto, que me llevó una semana resolver. Tenía una clase base y una clase que heredaba de ella. Por diversión y originalidad, llamémoslas Base y Derivada. Y su implementación era algo así:
Este código estuvo funcionando durante mucho tiempo y mucha gente se interesaba por saber quiénes eran esas clases, y llamaba a WhoAmI().
Después de un tiempo, tuvimos que aumentar el número de valores que podía soportar ‘parameter’ para la clase Derived. Un programador lo cambió para que fuera de tipo long en lugar de int, pero sólo para la clase Derivada, sin darse cuenta de que este método es un override del método Base.
No los culpo. Esto sólo se podía notar yendo a la clase base y comprobando activamente si había algún método con la misma firma.
De todos modos, ahora el código derivado se veía así:
¡Pero espera! De esta manera, la clase derivada no anula la base WhoAmI(), en lugar de sobrecargarla.
¿Qué sucede ahora cuando se llama a WhoAmI() de una clase derivada con un parámetro int? Se llama a Base::WhoAmI(). ¡Un completo desastre!
Que es exactamente lo que ocurría la mayor parte del tiempo en nuestro producto, excepto en ese nuevo lugar donde el programador lo llamó explícitamente con un long.
Es difícil rastrear este tipo de error, especialmente si su método no imprime quién lo llama, sino que cambia el estado del objeto. Me tomó casi una semana y mucho café para resolver esto.
¿Qué se podría hacer para evitar esto?
Hay un especificador que podría ahorrarle todos estos problemas: override. Úselo en su clase Derivada así:
En este caso, si un día necesitas cambiar el tipo del parámetro a long, y no actualizas la clase Base, tu programa no compilará, y te ahorrarás esta incómoda búsqueda de una semana:
Así que por favor, por favor, usa override. Es muy fácil y te ahorra muchos problemas en el futuro.
Espero que adoptes estas prácticas y hagas el mundo un poco mejor dejando un código más libre de errores y fácil de mantener.
Leave a Comment