Anuncios

Bienvenidos sean a este post, hoy veremos una serie de buenas practicas.

Anuncios

En el post anterior hablamos sobre RCA pero hay un refran que dice: No hay mejor defensa que el ataque. Para ello, es mejor evitar todo esto mediante buenas practicas que estar analizando la causa del problema. Si bien aplicar buenas practicas no nos asegura que no ocurran fallos si los lleva a su minima expresion pero no debemos dejar afuera a RCA para ser aplicado en los minimos problemas que pueden quedar. Cuando nos referimos a buenas practicas, son mas unas reglas que nos ayudaran a evitar dejar ciertas partes del codigo puedan evolucionar en causa raiz de un problema. Estas pueden ser divididas en bajo y alto nivel, veamos los elementos a inspeccionar en las reglas de bajo nivel:

  • Variables sin iniciar
  • Divisiones de enteros
  • Usar erroneamente a = en lugar de ==
  • Posibilidad de asignar una variable signed a una variable unsigned
  • Olvidar break en los switch
  • Efectos secundarios en expresiones compuestas o llamadas de funcion
Anuncios

Cuando pasemos a las reglas de alto nivel, debemos observar los siguientes temas:

  • Interfaces
  • Gestion de recursos
  • Gestion de memoria
  • Concurrencia
Anuncios
Anuncios

B. Stroustrup y H. Sutter sugieren seguir estas reglas en su documento online C++ Core Guidelines (Release 0.8), donde la seguridad de tipos estaticos y la seguridad de recursos es enfatizado. Ellos tambien hacen enfasis en la posibilidad de chequeos de rango para evitar punteros nulos desreferenciados, punteros flotantes, y el uso sistematico de excepciones.. Si las seguimos nos permitira tener un codigo estaticamente type-safe, sin fuga de recursos, tambien capturara muchos de los posibles errores logicos y puede lograr hasta correr mas rapido. No llegaremos a cubrir todas estas reglas pero si las mas importantes para nuestros codigos, pasemos a la primera.

Anuncios

Variables sin iniciar

Una variable sin iniciar es uno de los errores mas comunes de un desarrollador. Cuando declaramos una variables, automaticamente se reservara un espacio en memoria para esta. Si esta no es iniciada igualmente tendra un valor, puede tomar el valor de 0, pero tampoco tendremos una forma de poder predecirlo. Y esto puede afectar a la conducta del programa. Veamos el siguiente codigo:

#include <iostream>

int main()
{
        int arr[] = {10,2,5,1,9,8,7};
        int x;
        for(int i=0; i < 7; i++)
        {
                if (x > arr[i]) x = arr[i];
        }
        std::cout << "Valor minimo: " << x << std::endl;
        return 0;
}
Anuncios

Este codigo nos devuelve el valor minimo que tiene un array. Para ello pasa por todas las posiciones y compara si el valor de x es mayor al de la posicion, si es asi procede a asignarselo y asi hasta el final. Para finalmente mostrar el valor, compilemos y veamos la salida:

$ ./var
Valor minimo: 0
$
Anuncios

Nos devolvio un valor que no existe porque como comentamos anteriormente tomo el valor de 0 y al compararlo con el resto, ninguno sera mayor que este. Para solucionarlo tenemos dos opciones, le ponemos un valor cualquiera para compararlo o una mejor opcion como es la siguiente:

#include <iostream>
#include <climits>

int main()
{
        int arr[] = {10,2,5,1,9,8,7};
        int x = INT_MAX;
        for(int i=0; i < 7; i++)
        {
                if (x > arr[i]) x = arr[i];
        }
        std::cout << "Valor minimo: " << x << std::endl;
        return 0;
}
Anuncios

Primero, vamos a incluir esta libreria para acceder a nuevas constantes. La siguiente modificacion es en la variable x, donde ahora la iniciaremos con la constante INT_MAX para que se inicie con el valor maximo que puede recibir un entero. Por lo tanto, cualquier valor que comparemos siempre van a ser menores. El resto del codigo sigue siendo el mismo, compilemos y veamos como trabaja ahora:

$ ./var
Valor minimo: 1
$
Anuncios

Como pueden ver ahora si funciono, cuando no iniciamos una variable puede suceder que no nos afecte porque no es critico que tenga el valor de 0 pero como vimos en el codigo anterior, una variable no iniciada puede llevarnos a que el codigo falle en su tarea. Pasemos al siguiente tema.

Anuncios

Efectos colaterales en expresiones compuestas

Cuando una instruccion, operador o funcion han finalizado, estos podria seguir existiendo o dentro de su entorno compuesto. Esto tiene algunos efectos secundarios y pueden derivar en conductas indefinidas. Analicemos el siguiente ejemplo:

#include <iostream>

int f(int x, int y)
{
  return x*y;
}

int main()
{
  int x = 3;
  std::cout << f(++x, x) << std::endl;
}
Anuncios

En este codigo, primero tenemos una funcion que recibe dos valores y los multiplica entre si, devolviendo el resultado. En el main, definimos una variable con un valor. Luego llamamos a la funcion pero incrementamos el valor de la variable con ese operador. Pero esto hara que se calcule la multiplicacion de 4 x 4 pero por ahi en realidad queremos hacer 4 x 3. Esto es asi porque el operador hace x = x + 1 y ya lo modifica. Para hacer la otra operacion deberian haber usado x + 1 para que pase ese valor y no lo modifique. Es simplemente tener cuidado de como usamos a los operadores.

Anuncios

Mezclando signed y unsigned

Usualmente todos los operadores requiere que los operandos involucrados sean del mismo tipo. Si los dos operandos de distintos tipos, uno de ellos sera promovido al otro tipo. Veamos las tres posibles reglas que se pueden aplicar a esta situacion:

  • Cuando mezclamos tipos del mismo rango, el tipo signed sera promovido a tipo unsigned
  • Cuando mezclamos tipos de diferentes rangos, el de rango sera promovido al tipo de rango alto si todos los valores del lado de rango bajo pueden ser representados por el lado del rango alto
  • Pero si todos los valores del rango bajo del caso anterior no pueden ser representados, se usara la version unsigned del lado del rango alto
Anuncios

Para entender este concepto veamos el siguiente ejemplo:::

#include <iostream>

int main()
{
        int32_t x = 10;
        uint32_t y = 20;
        uint32_t z = x - y;
        std::cout << z << std::endl;

        return 0;
}
Anuncios

Lo que comentammos, tenemos una variable de tipo int y luego otra de tipo int pero unsigned. En la tercer variable almacenaremos la resta de las dos anteriores. En este caso, son del mismo rango pero distintos tipo y por lo tanto transformara a x de mismo tipo que y. Esto producira algo o no? En teoria deberia devolver -10, correcto? Compilemos y veamos como es la salida:

$ ./entero
4294967286
$
Anuncios

Como podemos ver, fallo estrepitosamente. Esto se debe a que unsigned int no puede representar al valor de -10 y su valor hexadecimal 0xFFFFFFF6 sera interpretado como UINT_MAX – 9 y no es otra cosa que el valor devuelto por el codigo. Esto sucede mas de lo que la mayoria cree y nos puede llevar a errores increibles en nuestros codigos.

Anuncios

Orden de evaluacion

Esta relacionado con el orden de la iniciacion de los miembros de la clase en su constructor. Analicemos el siguiente ejemplo:

class A {
public:
  A(int x) : v2(v1), v1(x) {
  };
  void print() {
    std::cout << "v1=" << v1 << ",v2=" << v2 << endl;
  };
protected:
  int v1, v2; 
};
Anuncios

Tenemos dos propiedades que no son privadas pero observen como son declaradas y como son iniciadas en el constructor. Esto puede ser algo confuso, no para el compilador pero si para otros desarrolladores, y si bien se puede iniciar de manera correcta ambos valores para mejorarlo debemos cambiarlo de la siguiente manera pero veremos otra particularidad:

class A {
public:
  B(int x) : v1(x), v2(v1) {};

  B(float x) : v2(x), v1(v2) {};
  void print() {
    cout << "v1=" << v1 << ", v2=" << v2 << endl;
  };

protected:
  int v1;
  int v2;
};
Anuncios
Anuncios

Al separar las propiedades ya no sera confuso y sabemos que valor tomara de los recibidos desde el constructor. Ahora nuestro constructor para iniciar correctamente los valores los debe asignar en el mismo orden de como son declarados. Por lo tanto, siempre v1 debe recibir el valor antes de asignarse a v2. Ahora veamos la sobrecarga del constructor, la hacemos para recibir valores de tipo float pero observen como los iniciamos. Como comentamos anteriormente, al ser iniciado de esta manera nos establecera un valor de v1 como sin iniciar y tomara el valor de -858993460. En lugar del valor asignado en v2 pero por el simple hecho de que no tiene ningun valor asignado al ser utilizado y como no es int le pasa un valor hexadecimal y lo convierte. Y esto puede provocar errores al momento de realizar una depuracion o en ejecucion. Pasemos al siguiente tema.

Anuncios

Chequeo en compilacion versus Chequeo en ejecucion

Hace un tiempo hablamos sobre como se puede realizar el chequeo al momento de compilacion y como nos podia traer algunos beneficios. Analicemos el siguiente ejemplo:

int nBits = 0;
for (int i = 1; i; i <<= 1){
     ++nBits;
}

if (nBits < 32){
    std::cerr << "int too small\n";
}
Anuncios

Esta parte del codigo contara la cantidad de bits de un entero y solo continuara si es mayor a 32 bits pero como esto depende del S.O podemos tener valores de 16 bits y puede fallar al momento de ejecucion. Reemplacemos todo eso con la siguiente linea:

static_assert(sizeof(int) >= 4);
Anuncios

En este caso, evalua si el tamaño es mayor o igual a 4 bytes, lo cual equivale a 32 bits, y este chequeo lo hacemos al momento de compilacion pero lo mas practico seria reemplazar a int con int32_t para asegurarnos que sea de 32 bits. Veamos otro ejemplo:

void interpretar(int* p, int n);
...
int v[10];
interpretar(v, 100);
Anuncios

Tenemos una funcion que lee una cantidad maxima de numeros enteros (n) en el puntero p. Para luego crear un array con 10 elementos pero en la funcion pasamos que lea 100. Obviamente, esto nos devolvera una excepcion porque salimos del rango de elementos pero al momento de compilarlo no nos informara de esto. Tomemos este codigo y hagamos el siguiente cambio:

void read_into(span<int> buf);
...
int v[10];
read_into(v);
Anuncios

Mediante span leera en un rango de numeros enteros del elemento recibido como argumento. Ahora en lugar de tener que recibir la cantidad de elementos a leer lo hara automaticamentte durante la compilacion. Por esta razon, muchas veces es preferible que los chequeos y tareas los haga al momento del compilarlo y asi evitar errores al momento de ejecutarlo.

Anuncios

Evitemos fugas de memoria

Una fuga de memoria (memory leak) significa que memoria ubicada de forma dinamica nunca debe ser liberada. Para manejar memoria dinamicamente tenemos a new y delete o delete[] para asignar y eliminar respectivamente. A su vez, en su momento hablamos que podemos reducir a su minima expresion la cantidad de fugas de memoria mediante el uso de smart pointers y RAII, pero asi y todo necesitamos seguir algunas reglas para crear un codigo de alta calidad.

Anuncios
Nota:
Sobre un smart pointer hablamos en este post y sobre RAII hablamos en este post.
Anuncios

El manejo mas facil de memoria es aquella que no ubicamos con nuestro codigo. Tomemos como ejemplo a T x, no escribas:

T* x = new T(); 
Anuncios

o

shared_ptr<T> x(new T() );.
Anuncios

Como mencionamos debemos tratar de evitar de manejar laa memoria mediante nuestro codigo, veamos el siguiente ejemplo:

void f_pero_mal(){
	T* p = new T();
	... //trabajemos con p
	delete p;
}
Anuncios

En esta funcion creamos al puntero de T, trabajamos con el y al final de esta lo eliminamos. En la teoria, no habria ningun inconveniente pero que sucede si en mitad de camino ocurre algo y no llegamos a la ultima linea, ese puntero seguira existiendo en memoria y puede crear fugas de memoria. Veamos como deberia ser de manera correcta:

void f_mejor()
{
	std::auto_ptr<T> p(new T());
	... //trabajamos con p
}
Anuncios

Aqui aplicamos el uso de RAII mediante el uso de smart pointers o punteros inteligentes. La ventaja mas importantes de manejarlo de esta manera es que sin importar lo que suceda en el bloque de la funcion cuando lleguemos al final, el puntero sera eliminado automaticamente. Y si bien, por necesidad necesitamos manejar la memoria con nuestro codigo, no lo hagamos manualmente. Para ello, podemos usar a la libreria container que nos facilitara mucho la tarea.

Anuncios

Recuerden que el uso de estas reglas no es obligatorio son siempre sugeridas pero el uso de las mismas puede permitirnos evitar fallos al momento de ejecucion, asi como tambien evitar que perdamos tiempo en RCA para descubrir porque falla. Recuerden que esto es parte de una guia que siempre pueden consultar en la siguiente URL:

https://isocpp.github.io/CppCoreGuidelines/

Anuncios

En resumen, hoy hemos visto una serie de buenas practicas y reglas para aplicar a nuestros codigos y evitar tener que usar RCA, asi como un detalle de algunas de estas con unos ejemplos que pueden sucedernos y traernos dolores de cabeza. Espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.

Anuncios
pp258

Donación

Es para mantenimento del sitio, gracias!

$1.50