Hola y bienvenido a mi lista de hábitos de novatos en C++.
C++ es un lenguaje increíblemente complejo con mucha historia.
Entonces, tanto si eres un novato
que realmente necesita estar atento a estas cosas
o si solo deseas mejorar un poco tu código, comencemos.
Cosa de novato número 1: Usar el «namespace» «std».
La gente generalmente lo usa para ahorrar tipeo.
Entonces, puedes decir cosas como «string» en lugar de «std::string».
Si se limita a una sola función, podría no ser tan malo.
Pero, seamos honestos, generalmente se usa a nivel global.
Peor aún, si haces esto en un archivo de encabezado,
también impones esta opción a todos los que usan tu código.
Por favor, no hagas esto en un archivo de encabezado...
y también considera valerte solo de los nombres que actualmente usas.
Número 2: usar «std::endl», especialmente en un «loop».
Probablemente quisiste simplemente imprimir una nueva línea,
pero «std::endl» hace más que eso.
También vacía el búfer, lo cual toma más tiempo.
En su lugar, simplemente usa el carácter de nueva linea.
Número tres: Usar un «for loop» con índice
en lugar un «for loop» basado en rangos, que expresa mejor la intención.
En este caso, no me importa en absoluto el índice.
En su lugar, uso un «for loop» basado en rangos.
No hay índice. Por lo tanto,
una posibilidad menos de un accidente tipográfico u otro error.
Número 4: Usar un «loop» cuando ya existe un algoritmo estándar
para hacer lo que estás tratando de llevar a cabo.
Hay algo simple que quieres hacer,
tal como encontrar el índice del primer número positivo en un vector.
Es tan simple que decides escribirlo tú mismo.
En cambio, examina si hay un algoritmo que ya hace lo que intentas hacer.
En este caso, podemos usar «std::find_if»
para encontrar dónde está el primer elemento positivo.
Número 5: Usar una matriz de estilo C
cuando podrías haber usado una matriz estándar.
Las matrices de estilo C a menudo se descomponen en punteros
y requieren que pases la longitud de la matriz junto con la propia matriz.
Esta es solo otra oportunidad para cometer un error de tipeo.
En su lugar, utiliza una matriz estándar.
Número 6: Cualquier uso de «reinterpret_cast».
Prácticamente lo único que puedes hacer con un objeto
que obtuviste de «reinterpret_cast»
es reinterpretarlo y devolverlo al tipo original.
Casi todo lo demás es un comportamiento indefinido.
Y sí, lo mismo ocurre con el «casting» de estilo C.
Entonces, lo siento por el famoso algoritmo Quake III
para calcular la raíz cuadrada inversa.
Reinterpretar los bytes de un «float» como «long»
es en realidad un comportamiento indefinido.
Siempre se te permite reinterpretar la dirección de un objeto
como un tipo carácter.
Esto te permite inspeccionar o imprimir los bytes que componen el objeto,
para que puedas ver su diseño de memoria.
Sin embargo, a partir de C++ 20,
este uso de «reinterpret_cast» tampoco es necesario.
«bit_cast» interpreta los bytes de un objeto
como un tipo diferente.
En este caso, puedo convertir cualquier objeto
en una matriz de bytes del mismo tamaño.
Número 7: Desechar «const».
Esta función toma un mapa
que asigna cadenas por la cantidad de veces que han ocurrido.
Toma dos palabras y luego te regresa la que tenga un conteo más alto.
La primera forma en que puedes intentar implementar esto
es buscar el contador de las dos palabras y luego,
si el primero es mayor que el segundo, devolver la primera palabra.
Si trato de compilar esto, aparece un mensaje de error extraño
que me dice que el método no está marcado como «const».
Y así es como terminamos con este código.
Sé que no estoy modificando el mapa. ¿Bien?
Lo correcto en este caso es no desechar «const».
Pero en su lugar, usa el método «at».
«at» es una versión «const» del operador «[]»,
que arroja si la palabra no está en el mapa.
Pero podrías preguntar:
¿Por qué no simplemente agregan una versión «const»
para que esto se compile?
Esto nos lleva a la cosa de novatos número 8:
No saber que el operador «[]» inserta el elemento en el mapa,
si este aún no existe.
Así es, simplemente tratando de buscar esta palabra en el mapa,
en realidad la inserta en el mismo con el contador en cero.
Número 9: Ignorar lo referente a «const correctness».
Esta función recorre un vector imprimiendo cada elemento,
un elemento por línea.
Eso no modifica el vector.
Entonces, podríamos y deberíamos marcar el vector «const».
Esto es doblemente importante para un parámetro de función,
para que quien la llama sepa
que puede usar esta función sin que se modifique su vector.
Número 10: No saber sobre la vida útil de los literales de cadena.
Se garantiza que los literales de cadena como este,
tendrán una vida útil igual a la del programa.
Por lo tanto, está perfectamente bien devolver esto,
aunque parezca que es una referencia a una variable local.
Número 11: No usar los «structured bindings».
Aquí tenemos un mapa de nombres de colores con sus valores hexadecimales.
Luego recorremos todos los pares
e imprimimos el nombre y el valor hexadecimal.
Sería mucho más legible, si pudiéramos referirnos a estos como "name" y "hex"
en lugar de "par.primero" y "par.segundo".
Bueno, eso es exactamente
lo que nos permite hacer un «structured binding».
Tomamos el par por referencia.
Luego, el «structured binding» introduce "name" y "hex"
como nombres para el primer y segundo elemento del par.
Los enlaces estructurados también se pueden usar con sus propios tipos,
si los miembros son públicos.
Los nombres se asignan a las variables según el orden en que son declaradas.
Número 12: Usar múltiples parámetros de salida
cuando deseas devolver varias cosas de una función.
En su lugar, crea una estructura básica y asigna nombres a esas cosas.
Entonces, puedes devolver los múltiples valores que deseabas,
al retornar la estructura en su lugar.
Quien hace la llamada puede hacer que parezca
que se devolvieron varios valores mediante el uso de «structured bindings».
Número 13: Hacer trabajos en tiempo de ejecución,
que podrían haberse hecho en tiempo de compilación.
Aquí hay una función que da la fórmula para la suma de los primeros n enteros.
A veces, los parámetros pueden conocerse en tiempo de compilación
y puedes hacer el cálculo antes de tiempo.
Adelante, hazle saber al compilador
que es totalmente correcto calcular esto con anticipación.
Número 14: Olvidar marcar los destructores virtuales.
Si una clase derivada se elimina a través de un puntero a esta clase base,
entonces no se llamará al destructor de clases derivadas,
solo se llamará al destructor de clases base.
Aquí tengo una clase que se deriva de la base
que no tiene un destructor virtual.
Esta función espera un puntero a la clase base.
Utiliza alguna funcionalidad básica y luego elimina el puntero cuando termina.
Esto sucederá automáticamente ya que estoy usando un puntero único.
Pero lo mismo sería cierto si solo tomara un puntero normal
y luego llamara manualmente a eliminar.
Si pasas un puntero a una instancia de este tipo derivado,
al final se llama al destructor incorrecto.
Para asegurarte de llamar al destructor correcto,
incluso a través de un puntero a una clase base,
debes marcar la función como virtual.
También es una buena práctica
marcar explícitamente al destructor de clases derivadas como anulado.
Número 15: Pensar que los miembros de la clase se inicializan
en el orden en que aparecen en la lista de inicializadores.
Al leer de izquierda a derecha, esto se ve bien.
El orden en el que se inicializan los miembros,
es el orden en el que se declaran.
Primero, inicializamos "m_end" como "m_start + size"
pero este "m start" es basura, todavía no se ha inicializado.
Por supuesto, podemos arreglar esto declarando primero "m_start".
O, dado que "start" también es un parámetro de la función,
podríamos simplemente definir la variable "m_end"
en términos del parámetro "start".
Número 16: No darse cuenta de que hay una diferencia
entre inicialización por defecto e inicialización con valor.
"x" y "x2" son inicializados por defecto.
Contienen basura hasta que les colocas algo.
"y", "y2" and "y3" son inicializados con valor.
Está garantizado que contienen el valor cero.
"z", por otro lado,
no está inicializado por defecto ni inicializado con valor.
Esta es una declaración de función.
Incluso si es un poco menos eficiente,
inicializar los valores casi siempre es una buena idea.
Lo mismo ocurre si tienes un tipo de agregado o de matriz.
En los dos primeros casos, "n" y "m" serán basura y "S" será la cadena vacía.
En los últimos tres casos, "n" y "m" son cero y "S" es la cadena vacía.
Ten en cuenta que, incluso con la inicialización por defecto,
la cadena vacía aún así fue inicializada.
Esto se debe a que,
tanto para la inicialización por defecto como para la inicialización con valor,
si defines un constructor predeterminado, este será llamado.
Número 17: Uso excesivo de números mágicos.
Introducir una constante básica en tu código
puede hacerlo muchísimo más legible.
El compilador lo optimizará de todos modos, solo dale un buen nombre.
Número 18: Intentar agregar o eliminar elementos de un contenedor
mientras se ejecuta un «loop» sobre él.
Bueno, a veces es necesario hacer eso,
pero lo que quiero decir es que los novatos a menudo lo hacen incorrectamente.
Estamos tratando de poner una copia del vector al final del vector.
Agregar o eliminar un elemento del vector puede invalidar los iteradores del vector.
Por ejemplo, «push_back» podría necesitar cambiar el tamaño del vector
y mover todos los elementos a una nueva ubicación.
Después de mover el contenido del vector a una nueva ubicación,
no puedes esperar que el puntero final sea el mismo.
Te encontrarías exactamente con el mismo problema.
De hecho, probablemente sea más claro el por qué te toparías con este problema,
si usaras iteradores directamente.
Este es un caso en el que el uso de un «loop» con índice
realmente resuelve el problema.
No importa si el contenido de tu vector se mueve a otro lugar,
el i-ésimo elemento sigue siendo el i-ésimo elemento.
Número 19: Devolver una variable local movida.
Entiendo totalmente de dónde vienes.
Un vector puede ser un objeto grande y no deseas hacer una copia de él.
Entonces, sigue adelante e intenta sacarlo.
Si hubiera intentado devolver "v" directamente,
no habría existido copia ni movimiento.
En esta situación, eso se debe a la optimización del valor de retorno.
Pero, ¿qué pasa si el compilador no puede optimizar el valor de retorno?
En todos los casos, el movimiento es innecesario.
El compilador siempre sabe que puede mover una variable local.
Pero en algunos casos, esto impide activamente
la optimización del valor de retorno.
Entonces, es por eso que esta es una de las pocas reglas
en las que puedo decir que nunca debes hacer esto.
Lo que nos lleva al número 20:
Pensar que ese movimiento en realidad mueve algo.
Aquí hay una implementación del movimiento estándar.
Es cierto que «full templated definition»
puede resultar un tanto excesivo para asimilar todo a la vez.
Entonces, echemos un vistazo solo al caso «int».
«move» toma una referencia «int lvalue»,
«static» lo convierte en una referencia «rvalue» y lo devuelve.
Exactamente lo mismo sucede en la «or value overload».
Simplemente hace una conversión estática a un «rvalue» y lo devuelve.
Un nombre más preciso para «move»
es probablemente algo así como «cast_to_rvalue».
Número 21: Pensar que está garantizado
que el orden de evaluación sea de izquierda a derecha.
He aquí un ejemplo famoso.
Tenemos una cadena que dice:
"but i have heard it works even if you don't believe in it".
Reemplazamos los primeros cuatro caracteres con una cadena vacía.
Luego, encontramos "even" y lo reemplazamos con "only".
Luego, encontramos "don't" y lo eliminamos.
Con este razonamiento, deberíamos terminar con:
"i have heard it works only if you believe in it".
Pero antes de C++ 17, el compilador podía calcular
cualquier subexpresión en cualquier orden.
Entonces, teóricamente, podría encontrar primero la ubicación de "even"
y luego reemplazar los primeros cuatro caracteres,
lo que hace que esa ubicación sea de cuatro.
Entonces, cuando ocurra el segundo reemplazo,
reemplazará estos cuatro caracteres con "only"
Y puedes ver adonde va esto, no obtienes el resultado que esperabas.
Pero, la buena noticia es que a partir de C++ 17,
este ejemplo está garantizado.
Si tienes "a.b", entonces se garantiza que "a" se evaluará antes que "b".
Sin embargo, incluso en C++ 20, aún no se garantiza que el orden
en que se evalúan los argumentos de la función,
sea de izquierda a derecha.
Esto no importaría mucho si "a", "b" y "c" fueran funciones puras.
Pero, si "a", "b" y "c" tienen efectos secundarios,
entonces el orden en que se llaman en realidad podría marcar la diferencia.
Número 22: Usar «heap allocations» totalmente innecesarias
cuando una «stack allocation» hubiese estado bien.
Aquí, creamos dos registros de clientes en la «heap».
Luego, hacemos algo de trabajo
y terminamos eliminando esas variables al final de la función.
La pregunta que debemos hacernos es:
¿realmente necesitaba ser una «heap allocation»?
Hay una buena posibilidad de que hubiera estado bien
con solo usar «stack allocation».
Entonces, digamos por el bien del argumento,
que estos objetos son demasiado grandes y realmente los quieres en la «heap».
Eso nos lleva al número 23:
No usar un puntero único ni un puntero compartido
para hacer tus «heap allocations».
¿Qué sucede si ocurre una excepción en el medio de esto?
Entonces, estas eliminaciones nunca ocurren y la memoria se pierde.
Cuando desees asegurarte de que se limpia un recurso,
debes colocar ese código de limpieza en un destructor.
Entonces, ¿por qué no creamos una clase que contenga un puntero
y luego en su destructor se elimina ese puntero?
Bueno, eso es exactamente lo que hace «unique_ptr».
Puedes asignarle un puntero «heap allocation»
y cuando salga del alcance, lo elimina.
Un puntero compartido, por otro lado, usa un esquema de conteo de referencias,
similar a lo que podrías tener en un lenguaje como Python.
Cuando el recuento de referencias llega a cero
porque el último puntero compartido queda fuera del alcance,
ese puntero compartido está a cargo de la eliminación.
Este esquema es mucho más costoso,
porque el incremento y decremento de las referencias
tiene que hacerse atómicamente.
Eso nos lleva al número 24:
Construir un puntero único o compartido directamente
en lugar de usar «make_unique» o «make_shared».
«make_unique» y «make_shared» pasarán tus argumentos directamente
al constructor de tu objeto.
Número 25: Cualquier uso de «new» o «delete».
No hay razón para reescribir una funcionalidad que ya existe.
Aquí, estoy tratando de administrar la memoria de algún recurso
y luego eliminarlo cuando termine.
Eso ya lo hace un puntero único.
No trates asociar el propósito de tu clase con la idea de propiedad de un objeto.
Ese es un tema completamente aparte.
El puntero único y el puntero compartido, juntos,
cubren prácticamente todos los usos válidos de «new» o «delete».
Número 26: Cualquier tipo de intento de hacer una gestión manual de recursos.
Esta es más o menos la misma historia que con «new» y «delete».
Si te encuentras liberando, cerrando o gestionando manualmente
cualquier tipo de recurso, busca una clase que lo haga automáticamente.
Por cierto, esta idea de que los recursos
se cierren automáticamente en un destructor se llama RAII.
Significa "Resource Acquisition Is Initialization".
Pero realmente tiene más que ver
con garantizar que los recursos se liberen tras la destrucción.
Número 27: Pensar que los punteros normales son de alguna manera malos.
Aquí hay una función "max" básica.
Esta función solo lee de los punteros.
No le importa en absoluto quién se encarga de borrarlos.
Si tu función no tiene nada que ver con la propiedad de los objetos en cuestión,
entonces no hay necesidad de usar punteros inteligentes.
La realidad es que los punteros normales no son dueños de lo que están señalando.
Y esto no debe confundirse con la «const-ness» del puntero.
Esta función "add" agrega el origen al destino.
Pero esta función no está a cargo del tiempo de vida
de ninguno de los punteros.
Por supuesto, si estás interactuando con código C,
el código C no compartirá esta convención.
Una función C podría muy bien devolverte alguna «heap-allocated memory»
que esperas que elimine.
Sin embargo, ten cuidado.
Si tienes un puntero con «malloc», debes eliminarlo con «free».
No puedes dejar que el puntero único
intente eliminarlo con la eliminación integrada.
Sin embargo, aún puede usar un puntero único.
Solo necesita definir su propio "deleter" que use «free».
Número 28: Devolver un puntero compartido
cuando no estás seguro de que el objeto se compartirá.
Si quien llama obtuvo un puntero único,
es económico y fácil convertirlo en un puntero compartido,
si eso es lo que realmente necesita.
Incluso podrían asignar directamente el valor de retorno del puntero único
a un puntero compartido.
Pero si les devuelves un puntero compartido en primer lugar,
el daño ya estaría hecho,
si todo lo que realmente quisieran fuera un puntero único.
Número 29:
Pensar que el puntero compartido es seguro para subprocesos.
La parte «reference counting» del puntero compartido
es segura para subprocesos.
Aquí, creo un recurso compartido y lo paso a dos subprocesos de trabajo.
Debido a que el «reference counting» es seguro para subprocesos,
no hay peligro de que el objeto no se elimine o se elimine dos veces.
Sin embargo, solo la parte de «reference counting» es atómica.
Esta parte aquí donde estamos accediendo a la variable "x"
y el recurso no es atómico y no hay bloqueos.
Esta es una antigua «data race» de dos hilos
que intentan modificar la misma memoria.
Si deseas arreglar la «data race», debes arreglarla de la misma manera
que arreglarías cualquier otra «data race».
Y 30: confundir un puntero «const» con un puntero «to const».
El concepto de un puntero «const» versus puntero «to const» es bastante simple.
Pero, muchos novatos luchan por recordar cómo diferenciarlos sintácticamente.
La regla es que «const» se aplica
a lo que esté inmediatamente a su izquierda.
A menos que sea lo que esté más a la izquierda,
en cuyo caso se aplica a lo que esté a su derecha.
Así que aquí, «const» se aplica al «int», no al puntero.
Aquí, «const» se aplica al «int», no al puntero.
Y aquí, «const» se aplica al puntero, no al «int».
Número 31: Ignorar las advertencias del compilador.
Ignorarlas o apagarlas,
conduce con mucha frecuencia a un comportamiento indefinido.
Espero que hayas disfrutado de mi lista de hábitos de novatos en C++.
Como siempre, gracias a mis patrocinadores y donantes por apoyarme.
Nos vemos la próxima vez.