martes, 10 de mayo de 2016

Encapsulando bucles

Las lambdas nos llevan acompañando en C++ desde el 2011. Desde entonces su uso ha estado centrado en la interacción con funciones de la STL como find_if() o transform(), sin embargo nada nos impide crear nuestras propias estructuras.

La STL tiene actualmente numerosas utilidades que permiten, prácticamente, olvidarse de los bucles. La pega de estas utilidades es que son demasiado rígidas y no suelen ser útiles en ciertas ocasiones. Supongamos por ejemplo que tenemos que realizar un programa que realice diferentes operaciones con matrices. La clase matriz puede tener una interfaz como la siguiente:

class Matrix
{
public:
  Matrix(int rows, int cols);

  ~Matrix();

  int Rows() const;

  int Cols() const;

  void SetValue(int row, int col, int value);

  int Value(int row, int col) const;
};

Lo primero que podemos probar es a imprimir la matriz por la consola. Una primera versión de la función podría ser la siguiente:

void PrintMatrix(const Matrix& matrix)
{
  auto const maxRows = matrix.Rows();
  auto const MaxCols = matrix.Cols();

  for( auto row=0; row<maxRows; ++row )
  {
    for( auto col=0; col<MaxCols; ++col )
    {
      std::cout << matrix.Value(row,col) << " ";
    }
    std::cout << std::endl;
  }
}

Si ahora queremos implementar una función para rellenar la matriz desde la consola podemos hacer lo siguiente:

void SetMatrix(Matrix& matrix)
{
  auto const maxRows = matrix.Rows();
  auto const MaxCols = matrix.Cols();

  for( auto row=0; row<maxRows; ++row )
  {
    for( auto col=0; col<MaxCols; ++col )
    {
      int value;
      std::cout << "Value for (" << row << "," << col << "): ";
      std::cin >> value;
      matrix.SetValue(row,col,value);
    }
  }
}

Un repaso rápido a estas dos funciones nos basta para ver que hay un patrón claro. Para iterar sobre la matriz hace falta usar dos bucles. ¿Qué tal si aplicamos un diseño reutilizable para no tener que escribir los dos bucles en cada ocasión?. La solución puede ser tan sencilla como usar una función que encapsule el bucle:

void MatrixIteration(const Matrix& matrix, auto func)
{
  auto const maxRows = matrix.Rows();
  auto const MaxCols = matrix.Cols();

  for( auto row=0; row<maxRows; ++row )
  {
    for( auto col=0; col<MaxCols; ++col )
    {
      func(row,col);
    }
  }
}

La función MatrixIteration recorre de forma iterativa todas las posiciones de la matriz y ejecuta un código externo en cada iteración. La gracia de este diseño es que admite el uso de lambdas para proporcionar el código a ejecutar en cada iteración. Con esta base las dos funciones anteriores podrían quedar así:

void PrintMatrix(const Matrix& matrix)
{
  const char* sep = " \n";

  MatrixIteration(matrix,[&](auto row, auto col)
  {
    std::cout << matrix.Value(row,col)
              << sep[(col<(matrix.Cols()-1))?0:1];
  });
}

void SetMatrix(Matrix& matrix)
{
  MatrixIteration(matrix,[&](auto row, auto col)
  {
    int value;
    std::cout << "Value for (" << row << "," << col << "): ";
    std::cin >> value;
    matrix.SetValue(row,col,value);
  });
}

Lo que hemos conseguido con MatrixIteration es aislar el bucle del código a ejecutar en cada iteración. Esta operativa tiene varias ventajas:

  • Únicamente hay una implementación para el bucle, luego es sencillo optimizar el mismo y que dichas optimizaciones se apliquen en todos los usos automáticamente.
  • El código que se ejecuta en cada iteración no puede manipular el bucle, luego evitamos que determinados errores en la implementación desemboquen en bucles infinitos o acceso a índices fuera de rango.
  • Eliminamos código innecesario en la función, dejando en la misma únicamente el verdadero núcleo de la misma.

También podemos proporcionar una segunda versión de la función que permita abortar los bucles. Imaginemos que una funcionalidad determinada tiene que poner todos los valores de la matriz a 0 hasta que encuentre un 1. Una implementación clásica podría lucir así:

void func(Matrix& matrix)
{
  auto abort = false;

  auto const maxRows = matrix.Rows();
  auto const MaxCols = matrix.Cols();

  for(auto row=0; row<maxRows && !abort; ++row)
  {
    for(auto col=0; col<MaxCols && !abort; ++col)
    {
      abort = (matrix.GetValue(row,col)==1);

      if (!abort)
        matrix.SetValue(row,col,0);
    }
  }
}

Sin embargo, aplicando lo comentado a lo largo de este artículo el código podría quedar tal que:

void MatrixIterationWithAbort(const Matrix& matrix, auto func)
{
  auto abort = false;

  auto const maxRows = matrix.Rows();
  auto const MaxCols = matrix.Cols();

  for( auto row=0; row<maxRows; ++row )
  {
    for( auto col=0; col<MaxCols; ++col )
    {
      func(row,col,abort);

      if( abort ) return;
    }
  }
}

void func(Matrix& matrix)
{
  MatrixIterationWithAbort(matrix,
                           [&](auto row, auto col, auto& abort)
  {
    abort = (matrix.GetValue(row,col)==1);

    if (!abort)
      matrix.SetValue(row,col,0);
  });
}

Como MatrixIterationWithAbort es una función podemos permitirnos el lujo de abandonar el bucle con un return. También se podría hacer comprobando el valor en cada iteración, pero mi intención era reflejar esta nueva posibilidad. Por lo demás, la forma de trabajar de la función no difiere en gran medida de la primera versión, MatrixIteration".

Últimos ajustes

Hasta ahora estamos manejando dos funciones diferentes para la gestión de los bucles. Lo ideal sería que las dos funciones tuviesen la misma firma, de forma que se eligiese una u otra versión en función de la firma propia de la lambda.

Hay formas de conseguir esto último. Una de ellas pasa por usar std::function de tal forma que ambas funciones tengan diferente firma para permitir la sobrecarga:

void MatrixIteration(const Matrix& matrix,
                     std::function<void(int,int)> func)
{
  auto const maxRows = matrix.Rows();
  auto const MaxCols = matrix.Cols();

  for( auto row=0; row<maxRows; ++row )
  {
    for( auto col=0; col<MaxCols; ++col )
    {
      func(row,col);
    }
  }
}

void MatrixIteration(const Matrix& matrix,
                     std::function<void(int,int,bool&)> func)
{
  auto const maxRows = matrix.Rows();
  auto const MaxCols = matrix.Cols();

  auto abort = false;

  for( auto row=0; row<maxRows; ++row )
  {
    for( auto col=0; col<MaxCols; ++col )
    {
      func(row,col,abort);

      if( abort ) return;
    }
  }
}

Con este pequeño cambio el código pasa a lucir más limpio y uniforme:

void PrintMatrix(const Matrix& matrix)
{
  const char* sep = " \n";
  MatrixIteration(matrix,
                  [&](auto row, auto col)
  {
    std::cout << matrix.Value(row,col) << sep[(col<(matrix.Cols()-1))?0:1];
  });
}

void func(Matrix& matrix)
{
  MatrixIteration(matrix,
                  [&](auto row, auto col, auto& abort)
  {
    abort = (matrix.GetValue(row,col)==1);

    if (!abort)
      matrix.SetValue(row,col,0);
  });
}

No hay comentarios:

Publicar un comentario